Experiment BT support using Chrome web serial (#920)

* plumbing

* plumbing

* logging

* more notes

* fixing typing

* more plumbing

* more plumbing

* different baud rate

* talking to the brick

* first over the air drop

* fix buffer

* tweak paraetmers

* formatting fixing double upload

* reduce console.log

* cleanup

* add BLE button to download dialog

* changed label

* recover from broken COM port

* fix function call

* reduce log level

* adding ticks

* some help

* updated support matrix

* more docs

* updated browser help

* more docs

* add link

* add device

* added image
This commit is contained in:
Peli de Halleux 2019-09-27 06:53:26 -07:00 committed by GitHub
parent 6d940a9ec7
commit 352c1ca5ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 538 additions and 318 deletions

View File

@ -4,6 +4,7 @@
* [Troubleshoot](/troubleshoot) * [Troubleshoot](/troubleshoot)
* [EV3 Manager](https://ev3manager.education.lego.com/) * [EV3 Manager](https://ev3manager.education.lego.com/)
* [Bluetooth](/bluetooth)
* [Forum](https://forum.makecode.com) * [Forum](https://forum.makecode.com)
* [LEGO Support](https://www.lego.com/service/) * [LEGO Support](https://www.lego.com/service/)
* [FIRST LEGO League](/fll) * [FIRST LEGO League](/fll)

View File

@ -28,7 +28,7 @@ program to a **.uf2** file, which you then copy to the **@drivename@** drive. Th
### ~ hint ### ~ hint
Not seeing the **@drivename@** drive? Make sure to upgrade your firmware at https://ev3manager.education.lego.com/. Try these [troubleshooting](/troubleshoot) tips if you still have trouble getting the drive to appear. **Experimental support** for Bluetooth download is now available. Please read the [Bluetooth](/bluetooth) page for more information.
### ~ ### ~

51
docs/bluetooth.md Normal file
View File

@ -0,0 +1,51 @@
# Bluetooth
This page describes the procedure to download MakeCode program to the EV3 brick
over Bluetooth.
## ~ hint
### WARNING: EXPERIMENTAL FEATURES AHEAD!
Support for Bluetooth download relies on [Web Serial](https://wicg.github.io/serial/),
an experimental browser feature. Web Serial is a work [in progress](https://www.chromestatus.com/feature/6577673212002304);
it may change or be removed in future versions without notice.
By enabling these experimental browser features, you could lose browser data or compromise your device security
or privacy.
## ~
## Supported browsers
* Chrome desktop, version 77 and higher, Windows 10
* [Edge Insider desktop](https://www.microsoftedgeinsider.com), version 77 and higher, Windows 10
To make sure your browser is up to date, go to the '...' menu, click "Help" then "About".
## Machine Setup
* pair your EV3 brick with your computer over Bluetooth. This is the usual pairing procedure.
* go to [chrome://flags/#enable-experimental-web-platform-features](chrome://flags/#enable-experimental-web-platform-features) and **enable**
**Experimental Web Platform features**
![A screenshot of the flags page in chrome](/static/bluetooth/experimental.png)
## Download over Bluetooth
* go to the **beta** editor https://makecode.mindstorms.com/beta
* click on **Download** to start a file download as usual
* on the download dialog, you should see a **Bluetooth** button. Click on the
**Bluetooth** button to enable the mode.
* **make sure the EV3 brick is not running a program**
* click on **Download** again to download over bluetooth.
## Choosing the correct serial port
Unforunately, the browser dialog does not make it easy to select which serial port is the brick.
On Windows, it typically reads "Standard Serial over Bluetooth" and you may
have multiple of those if you've paired different bricks.
## Feedback
Please send us your feedback through https://forum.makecode.com.

View File

@ -92,6 +92,11 @@ You can share your projects by clicking on the **share** button in the top left
Sharing programs is also shown in the [Tips and Tricks](https://legoeducation.videomarketingplatform.co/v.ihtml/player.html?token=5c594c2373367f7870196f519f3bfc7a&source=embed&photo%5fid=35719472) video. Sharing programs is also shown in the [Tips and Tricks](https://legoeducation.videomarketingplatform.co/v.ihtml/player.html?token=5c594c2373367f7870196f519f3bfc7a&source=embed&photo%5fid=35719472) video.
### Can I use Bluetooth to transfer my program?
The official answer is currently no. That being said, we have **Experimental support** for Bluetooth download. Please read the [Bluetooth](/bluetooth) page for more information.
### Why can't I delete my program (*.uf2) files from the Brick? ### Why can't I delete my program (*.uf2) files from the Brick?
There's a bug in the firmware which prevents you from deleting the programs (``*.uf2`` files) from your EV3 Brick. There isn't a firmware update to fix this yet. There's a bug in the firmware which prevents you from deleting the programs (``*.uf2`` files) from your EV3 Brick. There isn't a firmware update to fix this yet.

BIN
docs/static/bluetooth/experimental.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -2,57 +2,198 @@
/// <reference path="../node_modules/pxt-core/built/pxtsim.d.ts"/> /// <reference path="../node_modules/pxt-core/built/pxtsim.d.ts"/>
import UF2 = pxtc.UF2; import UF2 = pxtc.UF2;
import { Ev3Wrapper } from "./wrap";
export let ev3: pxt.editor.Ev3Wrapper export let ev3: Ev3Wrapper
export function debug() { export function debug() {
return initAsync() return initHidAsync()
.then(w => w.downloadFileAsync("/tmp/dmesg.txt", v => console.log(pxt.Util.uint8ArrayToString(v)))) .then(w => w.downloadFileAsync("/tmp/dmesg.txt", v => console.log(pxt.Util.uint8ArrayToString(v))))
} }
// Web Serial API https://wicg.github.io/serial/
// chromium bug https://bugs.chromium.org/p/chromium/issues/detail?id=884928
// Under experimental features in Chrome Desktop 77+
enum ParityType {
"none",
"even",
"odd",
"mark",
"space"
}
declare interface SerialOptions {
baudrate?: number;
databits?: number;
stopbits?: number;
parity?: ParityType;
buffersize?: number;
rtscts?: boolean;
xon?: boolean;
xoff?: boolean;
xany?: boolean;
}
type SerialPortInfo = pxt.Map<string>;
type SerialPortRequestOptions = any;
declare class SerialPort {
open(options?: SerialOptions): Promise<void>;
close(): void;
readonly readable: any;
readonly writable: any;
//getInfo(): SerialPortInfo;
}
declare interface Serial extends EventTarget {
onconnect: any;
ondisconnect: any;
getPorts(): Promise<SerialPort[]>
requestPort(options: SerialPortRequestOptions): Promise<SerialPort>;
}
class WebSerialPackageIO implements pxt.HF2.PacketIO {
onData: (v: Uint8Array) => void;
onError: (e: Error) => void;
onEvent: (v: Uint8Array) => void;
onSerial: (v: Uint8Array, isErr: boolean) => void;
sendSerialAsync: (buf: Uint8Array, useStdErr: boolean) => Promise<void>;
private _reader: any;
private _writer: any;
constructor(private port: SerialPort, private options: SerialOptions) {
// start reading
this.readSerialAsync();
}
async readSerialAsync() {
this._reader = this.port.readable.getReader();
let buffer: Uint8Array;
while (!!this._reader) {
const { done, value } = await this._reader.read()
if (!buffer) buffer = value;
else { // concat
let tmp = new Uint8Array(buffer.length + value.byteLength)
tmp.set(buffer, 0)
tmp.set(value, buffer.length)
buffer = tmp;
}
if (buffer && buffer.length >= 6) {
this.onData(new Uint8Array(buffer));
buffer = undefined;
}
}
}
static isSupported(): boolean {
return !!(<any>navigator).serial;
}
static async mkPacketIOAsync(): Promise<pxt.HF2.PacketIO> {
const serial = (<any>navigator).serial;
if (serial) {
try {
const requestOptions: SerialPortRequestOptions = {};
const port = await serial.requestPort(requestOptions);
const options: SerialOptions = {
baudrate: 460800,
buffersize: 4096
};
await port.open(options);
if (port)
return new WebSerialPackageIO(port, options);
} catch (e) {
console.log(`connection error`, e)
}
}
throw new Error("could not open serial port");
}
error(msg: string): any {
console.error(msg);
throw new Error(lf("error on brick ({0})", msg))
}
async reconnectAsync(): Promise<void> {
if (!this._reader) {
await this.port.open(this.options);
this.readSerialAsync();
}
return Promise.resolve();
}
async disconnectAsync(): Promise<void> {
this.port.close();
this._reader = undefined;
this._writer = undefined;
return Promise.resolve();
}
sendPacketAsync(pkt: Uint8Array): Promise<void> {
if (!this._writer)
this._writer = this.port.writable.getWriter();
return this._writer.write(pkt);
}
}
function hf2Async() { function hf2Async() {
return pxt.HF2.mkPacketIOAsync() const pktIOAsync: Promise<pxt.HF2.PacketIO> = useWebSerial
.then(h => { ? WebSerialPackageIO.mkPacketIOAsync() : pxt.HF2.mkPacketIOAsync()
let w = new pxt.editor.Ev3Wrapper(h) return pktIOAsync.then(h => {
let w = new Ev3Wrapper(h)
ev3 = w ev3 = w
return w.reconnectAsync(true) return w.reconnectAsync(true)
.then(() => w) .then(() => w)
}) })
} }
let noHID = false let useHID = false;
let useWebSerial = false;
let initPromise: Promise<pxt.editor.Ev3Wrapper> export function initAsync(): Promise<void> {
export function initAsync() {
if (initPromise)
return initPromise
let canHID = false
if (pxt.U.isNodeJS) { if (pxt.U.isNodeJS) {
// doesn't seem to work ATM // doesn't seem to work ATM
canHID = false useHID = false
} else { } else {
const forceHexDownload = /forceHexDownload/i.test(window.location.href); const nodehid = /nodehid/i.test(window.location.href);
if (pxt.Cloud.isLocalHost() && pxt.Cloud.localToken && !forceHexDownload) if (pxt.Cloud.isLocalHost() && pxt.Cloud.localToken && nodehid)
canHID = true useHID = true;
} }
if (noHID) if(WebSerialPackageIO.isSupported())
canHID = false pxt.tickEvent("bluetooth.supported");
if (canHID) { return Promise.resolve();
}
export function canUseWebSerial() {
return WebSerialPackageIO.isSupported();
}
export function enableWebSerial() {
initPromise = undefined;
useWebSerial = WebSerialPackageIO.isSupported();
useHID = useWebSerial;
}
let initPromise: Promise<Ev3Wrapper>
function initHidAsync() { // needs to run within a click handler
if (initPromise)
return initPromise
if (useHID) {
initPromise = hf2Async() initPromise = hf2Async()
.catch(err => { .catch(err => {
console.error(err);
initPromise = null initPromise = null
noHID = true useHID = false;
return Promise.reject(err) useWebSerial = false;
// cleanup
let p = ev3 ? ev3.disconnectAsync().catch(e => {}) : Promise.resolve();
return p.then(() => Promise.reject(err))
}) })
} else { } else {
noHID = true useHID = false
useWebSerial = false;
initPromise = Promise.reject(new Error("no HID")) initPromise = Promise.reject(new Error("no HID"))
} }
return initPromise;
return initPromise
} }
// this comes from aux/pxt.lms // this comes from aux/pxt.lms
@ -61,8 +202,6 @@ const rbfTemplate = `
74617274696e672e2e2e0084006080XX00448581644886488405018130813e80427965210084000a 74617274696e672e2e2e0084006080XX00448581644886488405018130813e80427965210084000a
` `
export function deployCoreAsync(resp: pxtc.CompileResult) { export function deployCoreAsync(resp: pxtc.CompileResult) {
let w: pxt.editor.Ev3Wrapper
let filename = resp.downloadFileBaseName || "pxt" let filename = resp.downloadFileBaseName || "pxt"
filename = filename.replace(/^lego-/, "") filename = filename.replace(/^lego-/, "")
@ -107,27 +246,31 @@ export function deployCoreAsync(resp: pxtc.CompileResult) {
return Promise.resolve(); return Promise.resolve();
} }
if (noHID) return saveUF2Async() if (!useHID) return saveUF2Async()
return initAsync() pxt.tickEvent("bluetooth.flash");
let w: Ev3Wrapper;
return initHidAsync()
.then(w_ => { .then(w_ => {
w = w_ w = w_
if (w.isStreaming) if (w.isStreaming)
pxt.U.userError("please stop the program first") pxt.U.userError("please stop the program first")
return w.stopAsync() return w.reconnectAsync(false)
}) })
.then(() => w.stopAsync())
.then(() => w.rmAsync(elfPath)) .then(() => w.rmAsync(elfPath))
.then(() => w.flashAsync(elfPath, UF2.readBytes(origElfUF2, 0, origElfUF2.length * 256))) .then(() => w.flashAsync(elfPath, UF2.readBytes(origElfUF2, 0, origElfUF2.length * 256)))
.then(() => w.flashAsync(rbfPath, rbfBIN)) .then(() => w.flashAsync(rbfPath, rbfBIN))
.then(() => w.runAsync(rbfPath)) .then(() => w.runAsync(rbfPath))
.then(() => { .then(() => {
pxt.tickEvent("bluetooth.success");
return w.disconnectAsync() return w.disconnectAsync()
//return Promise.delay(1000).then(() => w.dmesgAsync()) //return Promise.delay(1000).then(() => w.dmesgAsync())
}).catch(e => { }).catch(e => {
// if we failed to initalize, retry pxt.tickEvent("bluetooth.fail");
if (noHID) useHID = false;
return saveUF2Async() useWebSerial = false;
else // if we failed to initalize, tell the user to retry
return Promise.reject(e) return Promise.reject(e)
}) })
} }

View File

@ -1,28 +1,18 @@
/// <reference path="../node_modules/pxt-core/built/pxteditor.d.ts"/> /// <reference path="../node_modules/pxt-core/built/pxteditor.d.ts"/>
/// <reference path="../node_modules/pxt-core/built/pxtsim.d.ts"/> /// <reference path="../node_modules/pxt-core/built/pxtsim.d.ts"/>
import { deployCoreAsync, initAsync } from "./deploy"; import { deployCoreAsync, initAsync, canUseWebSerial, enableWebSerial } from "./deploy";
pxt.editor.initExtensionsAsync = function (opts: pxt.editor.ExtensionOptions): Promise<pxt.editor.ExtensionResult> { pxt.editor.initExtensionsAsync = function (opts: pxt.editor.ExtensionOptions): Promise<pxt.editor.ExtensionResult> {
pxt.debug('loading pxt-ev3 target extensions...') pxt.debug('loading pxt-ev3 target extensions...')
const res: pxt.editor.ExtensionResult = { const res: pxt.editor.ExtensionResult = {
deployCoreAsync, deployCoreAsync,
showUploadInstructionsAsync: (fn: string, url: string, confirmAsync: (options: any) => Promise<number>) => { showUploadInstructionsAsync: (fn: string, url: string, confirmAsync: (options: any) => Promise<number>) => {
let resolve: (thenableOrResult?: void | PromiseLike<void>) => void;
let reject: (error: any) => void;
const deferred = new Promise<void>((res, rej) => {
resolve = res;
reject = rej;
});
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 // https://msdn.microsoft.com/en-us/library/cc848897.aspx
// "For security reasons, data URIs are restricted to downloaded resources. // "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" // 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 downloadAgain = !pxt.BrowserUtils.isIE() && !pxt.BrowserUtils.isEdge();
const docUrl = pxt.appTarget.appTheme.usbDocs; const docUrl = pxt.appTarget.appTheme.usbDocs;
const saveAs = pxt.BrowserUtils.hasSaveAs();
const htmlBody = ` const htmlBody = `
<div class="ui grid stackable"> <div class="ui grid stackable">
@ -84,7 +74,36 @@ pxt.editor.initExtensionsAsync = function (opts: pxt.editor.ExtensionOptions): P
hideAgree: false, hideAgree: false,
agreeLbl: lf("I got it"), agreeLbl: lf("I got it"),
className: 'downloaddialog', className: 'downloaddialog',
buttons: [downloadAgain ? { buttons: [canUseWebSerial() ? {
label: lf("Bluetooth"),
icon: "bluetooth",
className: "bluetooth focused",
onclick: () => {
pxt.tickEvent("bluetooth.enable");
enableWebSerial();
confirmAsync({
header: lf("Bluetooth enabled"),
hasCloseIcon: true,
hideCancel: true,
buttons: [{
label: lf("Help"),
icon: "question circle",
className: "lightgrey",
url: "/bluetooth"
}],
htmlBody: `
<p>
${lf("Please download again to send your code to the EV3 over Bluetooth.")}
</p>
<p>
${lf("You will be prompted to select a serial port.")}
${lf("On Windows, look for 'Standard Serial over Bluetooth link'.")}
${lf("If you have paired multiple EV3, you might have to try out multiple ports until you find the correct one.")}
</p>
`
})
}
} : undefined, downloadAgain ? {
label: fn, label: fn,
icon: "download", icon: "download",
className: "lightgrey focused", className: "lightgrey focused",

View File

@ -1,9 +1,8 @@
namespace pxt.editor {
import HF2 = pxt.HF2 import HF2 = pxt.HF2
import U = pxt.U import U = pxt.U
function log(msg: string) { function log(msg: string) {
pxt.log("EWRAP: " + msg) pxt.debug("EWRAP: " + msg)
} }
export interface DirEntry { export interface DirEntry {
@ -30,8 +29,8 @@ namespace pxt.editor {
let payload = buf.slice(8) let payload = buf.slice(8)
if (code == 1) { if (code == 1) {
let str = U.uint8ArrayToString(payload) let str = U.uint8ArrayToString(payload)
if (Util.isNodeJS) if (U.isNodeJS)
console.log("SERIAL: " + str.replace(/\n+$/, "")) pxt.debug("SERIAL: " + str.replace(/\n+$/, ""))
else else
window.postMessage({ window.postMessage({
type: 'serial', type: 'serial',
@ -39,7 +38,7 @@ namespace pxt.editor {
data: str data: str
}, "*") }, "*")
} else } else
console.log("Magic: " + code + ": " + U.toHex(payload)) pxt.debug("Magic: " + code + ": " + U.toHex(payload))
return return
} }
if (this.dataDump) if (this.dataDump)
@ -280,6 +279,3 @@ namespace pxt.editor {
return this.io.disconnectAsync() return this.io.disconnectAsync()
} }
} }
}

View File

@ -185,3 +185,8 @@
font-family: 'legoIcons' !important; font-family: 'legoIcons' !important;
content: "\f119" !important; content: "\f119" !important;
} }
.bluetooth {
background-color: #007EF4 !important;
color: white !important;
}