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:
parent
6d940a9ec7
commit
352c1ca5ec
@ -4,6 +4,7 @@
|
||||
|
||||
* [Troubleshoot](/troubleshoot)
|
||||
* [EV3 Manager](https://ev3manager.education.lego.com/)
|
||||
* [Bluetooth](/bluetooth)
|
||||
* [Forum](https://forum.makecode.com)
|
||||
* [LEGO Support](https://www.lego.com/service/)
|
||||
* [FIRST LEGO League](/fll)
|
||||
|
@ -28,7 +28,7 @@ program to a **.uf2** file, which you then copy to the **@drivename@** drive. Th
|
||||
|
||||
### ~ 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
51
docs/bluetooth.md
Normal 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.
|
@ -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.
|
||||
|
||||
### 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?
|
||||
|
||||
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
BIN
docs/static/bluetooth/experimental.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
211
editor/deploy.ts
211
editor/deploy.ts
@ -2,57 +2,198 @@
|
||||
/// <reference path="../node_modules/pxt-core/built/pxtsim.d.ts"/>
|
||||
|
||||
import UF2 = pxtc.UF2;
|
||||
import { Ev3Wrapper } from "./wrap";
|
||||
|
||||
export let ev3: pxt.editor.Ev3Wrapper
|
||||
export let ev3: Ev3Wrapper
|
||||
|
||||
export function debug() {
|
||||
return initAsync()
|
||||
return initHidAsync()
|
||||
.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() {
|
||||
return pxt.HF2.mkPacketIOAsync()
|
||||
.then(h => {
|
||||
let w = new pxt.editor.Ev3Wrapper(h)
|
||||
const pktIOAsync: Promise<pxt.HF2.PacketIO> = useWebSerial
|
||||
? WebSerialPackageIO.mkPacketIOAsync() : pxt.HF2.mkPacketIOAsync()
|
||||
return pktIOAsync.then(h => {
|
||||
let w = new Ev3Wrapper(h)
|
||||
ev3 = w
|
||||
return w.reconnectAsync(true)
|
||||
.then(() => w)
|
||||
})
|
||||
}
|
||||
|
||||
let noHID = false
|
||||
|
||||
let initPromise: Promise<pxt.editor.Ev3Wrapper>
|
||||
export function initAsync() {
|
||||
if (initPromise)
|
||||
return initPromise
|
||||
|
||||
let canHID = false
|
||||
let useHID = false;
|
||||
let useWebSerial = false;
|
||||
export function initAsync(): Promise<void> {
|
||||
if (pxt.U.isNodeJS) {
|
||||
// doesn't seem to work ATM
|
||||
canHID = false
|
||||
useHID = false
|
||||
} else {
|
||||
const forceHexDownload = /forceHexDownload/i.test(window.location.href);
|
||||
if (pxt.Cloud.isLocalHost() && pxt.Cloud.localToken && !forceHexDownload)
|
||||
canHID = true
|
||||
const nodehid = /nodehid/i.test(window.location.href);
|
||||
if (pxt.Cloud.isLocalHost() && pxt.Cloud.localToken && nodehid)
|
||||
useHID = true;
|
||||
}
|
||||
|
||||
if (noHID)
|
||||
canHID = false
|
||||
if(WebSerialPackageIO.isSupported())
|
||||
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()
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
initPromise = null
|
||||
noHID = true
|
||||
return Promise.reject(err)
|
||||
useHID = false;
|
||||
useWebSerial = false;
|
||||
// cleanup
|
||||
let p = ev3 ? ev3.disconnectAsync().catch(e => {}) : Promise.resolve();
|
||||
return p.then(() => Promise.reject(err))
|
||||
})
|
||||
} else {
|
||||
noHID = true
|
||||
useHID = false
|
||||
useWebSerial = false;
|
||||
initPromise = Promise.reject(new Error("no HID"))
|
||||
}
|
||||
|
||||
return initPromise
|
||||
return initPromise;
|
||||
}
|
||||
|
||||
// this comes from aux/pxt.lms
|
||||
@ -61,8 +202,6 @@ const rbfTemplate = `
|
||||
74617274696e672e2e2e0084006080XX00448581644886488405018130813e80427965210084000a
|
||||
`
|
||||
export function deployCoreAsync(resp: pxtc.CompileResult) {
|
||||
let w: pxt.editor.Ev3Wrapper
|
||||
|
||||
let filename = resp.downloadFileBaseName || "pxt"
|
||||
filename = filename.replace(/^lego-/, "")
|
||||
|
||||
@ -107,27 +246,31 @@ export function deployCoreAsync(resp: pxtc.CompileResult) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (noHID) return saveUF2Async()
|
||||
if (!useHID) return saveUF2Async()
|
||||
|
||||
return initAsync()
|
||||
pxt.tickEvent("bluetooth.flash");
|
||||
let w: Ev3Wrapper;
|
||||
return initHidAsync()
|
||||
.then(w_ => {
|
||||
w = w_
|
||||
if (w.isStreaming)
|
||||
pxt.U.userError("please stop the program first")
|
||||
return w.stopAsync()
|
||||
return w.reconnectAsync(false)
|
||||
})
|
||||
.then(() => w.stopAsync())
|
||||
.then(() => w.rmAsync(elfPath))
|
||||
.then(() => w.flashAsync(elfPath, UF2.readBytes(origElfUF2, 0, origElfUF2.length * 256)))
|
||||
.then(() => w.flashAsync(rbfPath, rbfBIN))
|
||||
.then(() => w.runAsync(rbfPath))
|
||||
.then(() => {
|
||||
pxt.tickEvent("bluetooth.success");
|
||||
return w.disconnectAsync()
|
||||
//return Promise.delay(1000).then(() => w.dmesgAsync())
|
||||
}).catch(e => {
|
||||
// if we failed to initalize, retry
|
||||
if (noHID)
|
||||
return saveUF2Async()
|
||||
else
|
||||
pxt.tickEvent("bluetooth.fail");
|
||||
useHID = false;
|
||||
useWebSerial = false;
|
||||
// if we failed to initalize, tell the user to retry
|
||||
return Promise.reject(e)
|
||||
})
|
||||
}
|
||||
|
@ -1,28 +1,18 @@
|
||||
/// <reference path="../node_modules/pxt-core/built/pxteditor.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.debug('loading pxt-ev3 target extensions...')
|
||||
const res: pxt.editor.ExtensionResult = {
|
||||
deployCoreAsync,
|
||||
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
|
||||
// "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 saveAs = pxt.BrowserUtils.hasSaveAs();
|
||||
|
||||
const htmlBody = `
|
||||
<div class="ui grid stackable">
|
||||
@ -84,7 +74,36 @@ pxt.editor.initExtensionsAsync = function (opts: pxt.editor.ExtensionOptions): P
|
||||
hideAgree: false,
|
||||
agreeLbl: lf("I got it"),
|
||||
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,
|
||||
icon: "download",
|
||||
className: "lightgrey focused",
|
||||
|
@ -1,21 +1,20 @@
|
||||
namespace pxt.editor {
|
||||
import HF2 = pxt.HF2
|
||||
import U = pxt.U
|
||||
import HF2 = pxt.HF2
|
||||
import U = pxt.U
|
||||
|
||||
function log(msg: string) {
|
||||
pxt.log("EWRAP: " + msg)
|
||||
}
|
||||
function log(msg: string) {
|
||||
pxt.debug("EWRAP: " + msg)
|
||||
}
|
||||
|
||||
export interface DirEntry {
|
||||
export interface DirEntry {
|
||||
name: string;
|
||||
md5?: string;
|
||||
size?: number;
|
||||
}
|
||||
}
|
||||
|
||||
const runTemplate = "C00882010084XX0060640301606400"
|
||||
const usbMagic = 0x3d3f
|
||||
const runTemplate = "C00882010084XX0060640301606400"
|
||||
const usbMagic = 0x3d3f
|
||||
|
||||
export class Ev3Wrapper {
|
||||
export class Ev3Wrapper {
|
||||
msgs = new U.PromiseBuffer<Uint8Array>()
|
||||
private cmdSeq = U.randomUint32() & 0xffff;
|
||||
private lock = new U.PromiseQueue();
|
||||
@ -30,8 +29,8 @@ namespace pxt.editor {
|
||||
let payload = buf.slice(8)
|
||||
if (code == 1) {
|
||||
let str = U.uint8ArrayToString(payload)
|
||||
if (Util.isNodeJS)
|
||||
console.log("SERIAL: " + str.replace(/\n+$/, ""))
|
||||
if (U.isNodeJS)
|
||||
pxt.debug("SERIAL: " + str.replace(/\n+$/, ""))
|
||||
else
|
||||
window.postMessage({
|
||||
type: 'serial',
|
||||
@ -39,7 +38,7 @@ namespace pxt.editor {
|
||||
data: str
|
||||
}, "*")
|
||||
} else
|
||||
console.log("Magic: " + code + ": " + U.toHex(payload))
|
||||
pxt.debug("Magic: " + code + ": " + U.toHex(payload))
|
||||
return
|
||||
}
|
||||
if (this.dataDump)
|
||||
@ -279,7 +278,4 @@ namespace pxt.editor {
|
||||
log(`disconnect`);
|
||||
return this.io.disconnectAsync()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -185,3 +185,8 @@
|
||||
font-family: 'legoIcons' !important;
|
||||
content: "\f119" !important;
|
||||
}
|
||||
|
||||
.bluetooth {
|
||||
background-color: #007EF4 !important;
|
||||
color: white !important;
|
||||
}
|
Loading…
Reference in New Issue
Block a user