graceful handling of bluetooth connectivity issues (#922)
* more cleanup * cleanup life cycle * more error checking
This commit is contained in:
parent
2563fd6031
commit
42454ecd57
@ -4,7 +4,12 @@
|
|||||||
import UF2 = pxtc.UF2;
|
import UF2 = pxtc.UF2;
|
||||||
import { Ev3Wrapper } from "./wrap";
|
import { Ev3Wrapper } from "./wrap";
|
||||||
|
|
||||||
export let ev3: Ev3Wrapper
|
export let ev3: Ev3Wrapper;
|
||||||
|
let confirmAsync: (options: any) => Promise<number>;
|
||||||
|
|
||||||
|
export function setConfirmAsync(cf: (options: any) => Promise<number>) {
|
||||||
|
confirmAsync = cf;
|
||||||
|
}
|
||||||
|
|
||||||
export function debug() {
|
export function debug() {
|
||||||
return initHidAsync()
|
return initHidAsync()
|
||||||
@ -59,14 +64,10 @@ class WebSerialPackageIO implements pxt.HF2.PacketIO {
|
|||||||
private _writer: any;
|
private _writer: any;
|
||||||
|
|
||||||
constructor(private port: SerialPort, private options: SerialOptions) {
|
constructor(private port: SerialPort, private options: SerialOptions) {
|
||||||
|
this.openAsync();
|
||||||
// start reading
|
|
||||||
this.readSerialAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async readSerialAsync() {
|
async readSerialAsync() {
|
||||||
if (this._reader) return;
|
|
||||||
|
|
||||||
this._reader = this.port.readable.getReader();
|
this._reader = this.port.readable.getReader();
|
||||||
let buffer: Uint8Array;
|
let buffer: Uint8Array;
|
||||||
const reader = this._reader;
|
const reader = this._reader;
|
||||||
@ -100,8 +101,6 @@ class WebSerialPackageIO implements pxt.HF2.PacketIO {
|
|||||||
baudrate: 460800,
|
baudrate: 460800,
|
||||||
buffersize: 4096
|
buffersize: 4096
|
||||||
};
|
};
|
||||||
await port.open(options);
|
|
||||||
if (port)
|
|
||||||
return new WebSerialPackageIO(port, options);
|
return new WebSerialPackageIO(port, options);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`connection error`, e)
|
console.log(`connection error`, e)
|
||||||
@ -115,27 +114,32 @@ class WebSerialPackageIO implements pxt.HF2.PacketIO {
|
|||||||
throw new Error(lf("error on brick ({0})", msg))
|
throw new Error(lf("error on brick ({0})", msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
private close() {
|
private openAsync() {
|
||||||
if (this.port.readable) {// it's open
|
console.log(`serial: opening port`)
|
||||||
|
if (!!this._reader) return Promise.resolve();
|
||||||
|
this._reader = undefined;
|
||||||
|
this._writer = undefined;
|
||||||
|
return this.port.open(this.options)
|
||||||
|
.then(() => {
|
||||||
|
this.readSerialAsync();
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private closeAsync() {
|
||||||
|
console.log(`serial: closing port`);
|
||||||
this.port.close();
|
this.port.close();
|
||||||
this._reader = undefined;
|
this._reader = undefined;
|
||||||
this._writer = undefined;
|
this._writer = undefined;
|
||||||
}
|
return Promise.delay(500);
|
||||||
}
|
}
|
||||||
|
|
||||||
async reconnectAsync(): Promise<void> {
|
reconnectAsync(): Promise<void> {
|
||||||
if (!this.port.readable) {
|
return this.openAsync();
|
||||||
this._reader = undefined;
|
|
||||||
this._writer = undefined;
|
|
||||||
await this.port.open(this.options);
|
|
||||||
this.readSerialAsync();
|
|
||||||
}
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async disconnectAsync(): Promise<void> {
|
disconnectAsync(): Promise<void> {
|
||||||
this.close();
|
return this.closeAsync();
|
||||||
return Promise.resolve();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sendPacketAsync(pkt: Uint8Array): Promise<void> {
|
sendPacketAsync(pkt: Uint8Array): Promise<void> {
|
||||||
@ -178,12 +182,23 @@ export function canUseWebSerial() {
|
|||||||
return WebSerialPackageIO.isSupported();
|
return WebSerialPackageIO.isSupported();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function enableWebSerial() {
|
export function enableWebSerialAsync() {
|
||||||
initPromise = undefined;
|
initPromise = undefined;
|
||||||
useWebSerial = WebSerialPackageIO.isSupported();
|
useWebSerial = WebSerialPackageIO.isSupported();
|
||||||
useHID = useWebSerial;
|
useHID = useWebSerial;
|
||||||
if (useWebSerial)
|
if (useWebSerial)
|
||||||
initHidAsync().done();
|
return initHidAsync().then(() => { });
|
||||||
|
else return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupAsync() {
|
||||||
|
if (ev3) {
|
||||||
|
console.log('cleanup previous port')
|
||||||
|
return ev3.disconnectAsync()
|
||||||
|
.catch(e => { })
|
||||||
|
.finally(() => { ev3 = undefined; });
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
let initPromise: Promise<Ev3Wrapper>
|
let initPromise: Promise<Ev3Wrapper>
|
||||||
@ -191,15 +206,14 @@ function initHidAsync() { // needs to run within a click handler
|
|||||||
if (initPromise)
|
if (initPromise)
|
||||||
return initPromise
|
return initPromise
|
||||||
if (useHID) {
|
if (useHID) {
|
||||||
initPromise = hf2Async()
|
initPromise = cleanupAsync()
|
||||||
|
.then(() => hf2Async())
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
initPromise = null
|
initPromise = null
|
||||||
useHID = false;
|
useHID = false;
|
||||||
useWebSerial = false;
|
useWebSerial = false;
|
||||||
// cleanup
|
return Promise.reject(err);
|
||||||
let p = ev3 ? ev3.disconnectAsync().catch(e => { }) : Promise.resolve();
|
|
||||||
return p.then(() => Promise.reject(err))
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
useHID = false
|
useHID = false
|
||||||
@ -210,6 +224,7 @@ function initHidAsync() { // needs to run within a click handler
|
|||||||
}
|
}
|
||||||
|
|
||||||
// this comes from aux/pxt.lms
|
// this comes from aux/pxt.lms
|
||||||
|
const fspath = "../prjs/BrkProg_SAVE/"
|
||||||
const rbfTemplate = `
|
const rbfTemplate = `
|
||||||
4c45474f580000006d000100000000001c000000000000000e000000821b038405018130813e8053
|
4c45474f580000006d000100000000001c000000000000000e000000821b038405018130813e8053
|
||||||
74617274696e672e2e2e0084006080XX00448581644886488405018130813e80427965210084000a
|
74617274696e672e2e2e0084006080XX00448581644886488405018130813e80427965210084000a
|
||||||
@ -218,8 +233,6 @@ export function deployCoreAsync(resp: pxtc.CompileResult) {
|
|||||||
let filename = resp.downloadFileBaseName || "pxt"
|
let filename = resp.downloadFileBaseName || "pxt"
|
||||||
filename = filename.replace(/^lego-/, "")
|
filename = filename.replace(/^lego-/, "")
|
||||||
|
|
||||||
let fspath = "../prjs/BrkProg_SAVE/"
|
|
||||||
|
|
||||||
let elfPath = fspath + filename + ".elf"
|
let elfPath = fspath + filename + ".elf"
|
||||||
let rbfPath = fspath + filename + ".rbf"
|
let rbfPath = fspath + filename + ".rbf"
|
||||||
|
|
||||||
@ -269,6 +282,26 @@ export function deployCoreAsync(resp: pxtc.CompileResult) {
|
|||||||
if (w.isStreaming)
|
if (w.isStreaming)
|
||||||
pxt.U.userError("please stop the program first")
|
pxt.U.userError("please stop the program first")
|
||||||
return w.reconnectAsync(false)
|
return w.reconnectAsync(false)
|
||||||
|
.catch(e => {
|
||||||
|
// user easily forgets to stop robot
|
||||||
|
if (confirmAsync)
|
||||||
|
return confirmAsync({
|
||||||
|
header: lf("Bluetooth download failed..."),
|
||||||
|
htmlBody:
|
||||||
|
`<ul>
|
||||||
|
<li>${lf("Make sure to stop your program on the EV3.")}</li>
|
||||||
|
<li>${lf("Check your battery level.")}</li>
|
||||||
|
</ul>`,
|
||||||
|
hasCloseIcon: true,
|
||||||
|
hideCancel: true,
|
||||||
|
hideAgree: false,
|
||||||
|
agreeLbl: lf("Try again"),
|
||||||
|
}).then(() => w.disconnectAsync())
|
||||||
|
.then(() => w.reconnectAsync());
|
||||||
|
|
||||||
|
// nothing we can do
|
||||||
|
return Promise.reject(e);
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.then(() => w.stopAsync())
|
.then(() => w.stopAsync())
|
||||||
.then(() => w.rmAsync(elfPath))
|
.then(() => w.rmAsync(elfPath))
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
/// <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, canUseWebSerial, enableWebSerial } from "./deploy";
|
import { deployCoreAsync, initAsync, canUseWebSerial, enableWebSerialAsync, setConfirmAsync } from "./deploy";
|
||||||
|
|
||||||
|
let bluetoothDialogShown = false;
|
||||||
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>) => {
|
||||||
|
setConfirmAsync(confirmAsync);
|
||||||
// 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"
|
||||||
@ -80,8 +82,12 @@ pxt.editor.initExtensionsAsync = function (opts: pxt.editor.ExtensionOptions): P
|
|||||||
className: "bluetooth focused",
|
className: "bluetooth focused",
|
||||||
onclick: () => {
|
onclick: () => {
|
||||||
pxt.tickEvent("bluetooth.enable");
|
pxt.tickEvent("bluetooth.enable");
|
||||||
|
if (bluetoothDialogShown) {
|
||||||
|
enableWebSerialAsync().done();
|
||||||
|
} else {
|
||||||
|
bluetoothDialogShown = true;
|
||||||
confirmAsync({
|
confirmAsync({
|
||||||
header: lf("Bluetooth enabled"),
|
header: lf("Bluetooth pairing"),
|
||||||
hasCloseIcon: true,
|
hasCloseIcon: true,
|
||||||
hideCancel: true,
|
hideCancel: true,
|
||||||
buttons: [{
|
buttons: [{
|
||||||
@ -90,19 +96,23 @@ pxt.editor.initExtensionsAsync = function (opts: pxt.editor.ExtensionOptions): P
|
|||||||
className: "lightgrey",
|
className: "lightgrey",
|
||||||
url: "/bluetooth"
|
url: "/bluetooth"
|
||||||
}],
|
}],
|
||||||
htmlBody: `
|
htmlBody: `<p>
|
||||||
<p>
|
|
||||||
${lf("Download again to send your code to the EV3 over Bluetooth. Make sure to stop your program!")}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
${lf("You will be prompted to select a serial port.")}
|
${lf("You will be prompted to select a serial port.")}
|
||||||
${lf("On Windows, look for 'Standard Serial over Bluetooth link'.")}
|
${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.")}
|
${lf("If you have paired multiple EV3, you might have to try out multiple ports until you find the correct one.")}
|
||||||
</p>
|
</p>
|
||||||
`
|
`
|
||||||
}).then(() => {
|
}).then(() => enableWebSerialAsync())
|
||||||
enableWebSerial();
|
.then(() => confirmAsync({
|
||||||
})
|
header: lf("Bluetooth is ready!"),
|
||||||
|
hasCloseIcon: true,
|
||||||
|
hideCancel: true,
|
||||||
|
htmlBody: `<p>
|
||||||
|
${lf("Click Download again to send your code to the EV3 over Bluetooth.")}
|
||||||
|
</p>
|
||||||
|
`
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} : undefined, downloadAgain ? {
|
} : undefined, downloadAgain ? {
|
||||||
label: fn,
|
label: fn,
|
||||||
|
Loading…
Reference in New Issue
Block a user