2018-01-04 01:18:39 +01:00
|
|
|
/// <reference path="../node_modules/pxt-core/built/pxteditor.d.ts"/>
|
|
|
|
/// <reference path="../node_modules/pxt-core/built/pxtsim.d.ts"/>
|
|
|
|
|
|
|
|
import UF2 = pxtc.UF2;
|
2019-09-27 15:53:26 +02:00
|
|
|
import { Ev3Wrapper } from "./wrap";
|
2018-01-04 01:18:39 +01:00
|
|
|
|
2019-09-27 20:15:10 +02:00
|
|
|
export let ev3: Ev3Wrapper;
|
|
|
|
let confirmAsync: (options: any) => Promise<number>;
|
|
|
|
|
|
|
|
export function setConfirmAsync(cf: (options: any) => Promise<number>) {
|
|
|
|
confirmAsync = cf;
|
|
|
|
}
|
2018-01-04 01:18:39 +01:00
|
|
|
|
|
|
|
export function debug() {
|
2019-09-27 15:53:26 +02:00
|
|
|
return initHidAsync()
|
2018-01-04 01:18:39 +01:00
|
|
|
.then(w => w.downloadFileAsync("/tmp/dmesg.txt", v => console.log(pxt.Util.uint8ArrayToString(v))))
|
|
|
|
}
|
|
|
|
|
2019-09-27 15:53:26 +02:00
|
|
|
|
|
|
|
// 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>;
|
2018-01-04 01:18:39 +01:00
|
|
|
}
|
|
|
|
|
2019-09-27 15:53:26 +02:00
|
|
|
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;
|
2018-01-04 01:18:39 +01:00
|
|
|
|
2019-09-27 15:53:26 +02:00
|
|
|
constructor(private port: SerialPort, private options: SerialOptions) {
|
2019-09-27 20:15:10 +02:00
|
|
|
this.openAsync();
|
2019-09-27 15:53:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async readSerialAsync() {
|
|
|
|
this._reader = this.port.readable.getReader();
|
|
|
|
let buffer: Uint8Array;
|
2019-09-27 18:16:27 +02:00
|
|
|
const reader = this._reader;
|
|
|
|
while (reader === this._reader) { // will change if we recycle the connection
|
2019-09-27 15:53:26 +02:00
|
|
|
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;
|
|
|
|
}
|
2018-01-04 01:18:39 +01:00
|
|
|
|
2019-09-27 15:53:26 +02:00
|
|
|
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
|
|
|
|
};
|
2019-09-27 20:15:10 +02:00
|
|
|
return new WebSerialPackageIO(port, options);
|
2019-09-27 15:53:26 +02:00
|
|
|
} 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))
|
|
|
|
}
|
|
|
|
|
2019-09-27 20:15:10 +02:00
|
|
|
private openAsync() {
|
|
|
|
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();
|
|
|
|
});
|
2019-09-27 18:16:27 +02:00
|
|
|
}
|
|
|
|
|
2019-09-27 20:15:10 +02:00
|
|
|
private closeAsync() {
|
|
|
|
console.log(`serial: closing port`);
|
|
|
|
this.port.close();
|
|
|
|
this._reader = undefined;
|
|
|
|
this._writer = undefined;
|
|
|
|
return Promise.delay(500);
|
2019-09-27 15:53:26 +02:00
|
|
|
}
|
|
|
|
|
2019-09-27 20:15:10 +02:00
|
|
|
reconnectAsync(): Promise<void> {
|
|
|
|
return this.openAsync();
|
|
|
|
}
|
|
|
|
|
|
|
|
disconnectAsync(): Promise<void> {
|
|
|
|
return this.closeAsync();
|
2019-09-27 15:53:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
sendPacketAsync(pkt: Uint8Array): Promise<void> {
|
|
|
|
if (!this._writer)
|
|
|
|
this._writer = this.port.writable.getWriter();
|
|
|
|
return this._writer.write(pkt);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function hf2Async() {
|
|
|
|
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 useHID = false;
|
|
|
|
let useWebSerial = false;
|
|
|
|
export function initAsync(): Promise<void> {
|
2018-01-04 01:18:39 +01:00
|
|
|
if (pxt.U.isNodeJS) {
|
2018-01-05 17:17:20 +01:00
|
|
|
// doesn't seem to work ATM
|
2019-09-27 15:53:26 +02:00
|
|
|
useHID = false
|
2018-01-04 01:18:39 +01:00
|
|
|
} else {
|
2019-09-27 15:53:26 +02:00
|
|
|
const nodehid = /nodehid/i.test(window.location.href);
|
|
|
|
if (pxt.Cloud.isLocalHost() && pxt.Cloud.localToken && nodehid)
|
|
|
|
useHID = true;
|
2018-01-04 01:18:39 +01:00
|
|
|
}
|
|
|
|
|
2019-09-27 18:16:27 +02:00
|
|
|
if (WebSerialPackageIO.isSupported())
|
2019-09-27 15:53:26 +02:00
|
|
|
pxt.tickEvent("bluetooth.supported");
|
2018-01-04 01:18:39 +01:00
|
|
|
|
2019-09-27 15:53:26 +02:00
|
|
|
return Promise.resolve();
|
|
|
|
}
|
|
|
|
|
|
|
|
export function canUseWebSerial() {
|
|
|
|
return WebSerialPackageIO.isSupported();
|
|
|
|
}
|
|
|
|
|
2019-09-27 20:15:10 +02:00
|
|
|
export function enableWebSerialAsync() {
|
2019-09-27 15:53:26 +02:00
|
|
|
initPromise = undefined;
|
|
|
|
useWebSerial = WebSerialPackageIO.isSupported();
|
|
|
|
useHID = useWebSerial;
|
2019-09-27 18:16:27 +02:00
|
|
|
if (useWebSerial)
|
2019-09-27 20:15:10 +02:00
|
|
|
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();
|
2019-09-27 15:53:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
let initPromise: Promise<Ev3Wrapper>
|
|
|
|
function initHidAsync() { // needs to run within a click handler
|
|
|
|
if (initPromise)
|
|
|
|
return initPromise
|
|
|
|
if (useHID) {
|
2019-09-27 20:15:10 +02:00
|
|
|
initPromise = cleanupAsync()
|
|
|
|
.then(() => hf2Async())
|
2018-01-04 01:18:39 +01:00
|
|
|
.catch(err => {
|
2019-09-27 15:53:26 +02:00
|
|
|
console.error(err);
|
2018-01-04 01:18:39 +01:00
|
|
|
initPromise = null
|
2019-09-27 15:53:26 +02:00
|
|
|
useHID = false;
|
|
|
|
useWebSerial = false;
|
2019-09-27 20:15:10 +02:00
|
|
|
return Promise.reject(err);
|
2018-01-04 01:18:39 +01:00
|
|
|
})
|
|
|
|
} else {
|
2019-09-27 15:53:26 +02:00
|
|
|
useHID = false
|
|
|
|
useWebSerial = false;
|
2018-01-04 01:18:39 +01:00
|
|
|
initPromise = Promise.reject(new Error("no HID"))
|
|
|
|
}
|
2019-09-27 15:53:26 +02:00
|
|
|
return initPromise;
|
2018-01-04 01:18:39 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// this comes from aux/pxt.lms
|
2019-09-27 20:15:10 +02:00
|
|
|
const fspath = "../prjs/BrkProg_SAVE/"
|
2018-01-04 01:18:39 +01:00
|
|
|
const rbfTemplate = `
|
|
|
|
4c45474f580000006d000100000000001c000000000000000e000000821b038405018130813e8053
|
|
|
|
74617274696e672e2e2e0084006080XX00448581644886488405018130813e80427965210084000a
|
|
|
|
`
|
2018-06-20 22:32:58 +02:00
|
|
|
export function deployCoreAsync(resp: pxtc.CompileResult) {
|
2018-01-04 01:18:39 +01:00
|
|
|
let filename = resp.downloadFileBaseName || "pxt"
|
|
|
|
filename = filename.replace(/^lego-/, "")
|
|
|
|
|
|
|
|
let elfPath = fspath + filename + ".elf"
|
|
|
|
let rbfPath = fspath + filename + ".rbf"
|
|
|
|
|
|
|
|
let rbfHex = rbfTemplate
|
|
|
|
.replace(/\s+/g, "")
|
|
|
|
.replace("XX", pxt.U.toHex(pxt.U.stringToUint8Array(elfPath)))
|
|
|
|
let rbfBIN = pxt.U.fromHex(rbfHex)
|
|
|
|
pxt.HF2.write16(rbfBIN, 4, rbfBIN.length)
|
|
|
|
|
2018-02-01 03:10:15 +01:00
|
|
|
let origElfUF2 = UF2.parseFile(pxt.U.stringToUint8Array(ts.pxtc.decodeBase64(resp.outfiles[pxt.outputName()])))
|
2018-01-04 01:18:39 +01:00
|
|
|
|
|
|
|
let mkFile = (ext: string, data: Uint8Array = null) => {
|
|
|
|
let f = UF2.newBlockFile()
|
|
|
|
f.filename = "Projects/" + filename + ext
|
|
|
|
if (data)
|
|
|
|
UF2.writeBytes(f, 0, data)
|
|
|
|
return f
|
|
|
|
}
|
|
|
|
|
|
|
|
let elfUF2 = mkFile(".elf")
|
|
|
|
for (let b of origElfUF2) {
|
|
|
|
UF2.writeBytes(elfUF2, b.targetAddr, b.data)
|
|
|
|
}
|
|
|
|
|
|
|
|
let r = UF2.concatFiles([elfUF2, mkFile(".rbf", rbfBIN)])
|
|
|
|
let data = UF2.serializeFile(r)
|
|
|
|
|
|
|
|
resp.outfiles[pxtc.BINARY_UF2] = btoa(data)
|
|
|
|
|
|
|
|
let saveUF2Async = () => {
|
2018-06-14 19:40:19 +02:00
|
|
|
if (pxt.commands && pxt.commands.electronDeployAsync) {
|
2018-06-14 19:33:53 +02:00
|
|
|
return pxt.commands.electronDeployAsync(resp);
|
2018-01-04 01:18:39 +01:00
|
|
|
}
|
2018-06-20 22:32:58 +02:00
|
|
|
if (pxt.commands && pxt.commands.saveOnlyAsync) {
|
2018-06-14 19:33:53 +02:00
|
|
|
return pxt.commands.saveOnlyAsync(resp);
|
|
|
|
}
|
|
|
|
return Promise.resolve();
|
2018-01-04 01:18:39 +01:00
|
|
|
}
|
|
|
|
|
2019-09-27 15:53:26 +02:00
|
|
|
if (!useHID) return saveUF2Async()
|
2018-01-04 01:18:39 +01:00
|
|
|
|
2019-09-27 15:53:26 +02:00
|
|
|
pxt.tickEvent("bluetooth.flash");
|
|
|
|
let w: Ev3Wrapper;
|
|
|
|
return initHidAsync()
|
2018-01-04 01:18:39 +01:00
|
|
|
.then(w_ => {
|
|
|
|
w = w_
|
|
|
|
if (w.isStreaming)
|
|
|
|
pxt.U.userError("please stop the program first")
|
2019-09-27 15:53:26 +02:00
|
|
|
return w.reconnectAsync(false)
|
2019-09-27 20:15:10 +02:00
|
|
|
.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);
|
|
|
|
})
|
2018-01-04 01:18:39 +01:00
|
|
|
})
|
2019-09-27 15:53:26 +02:00
|
|
|
.then(() => w.stopAsync())
|
2018-01-04 01:18:39 +01:00
|
|
|
.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(() => {
|
2019-09-27 15:53:26 +02:00
|
|
|
pxt.tickEvent("bluetooth.success");
|
2018-06-20 22:32:58 +02:00
|
|
|
return w.disconnectAsync()
|
2018-01-04 01:18:39 +01:00
|
|
|
//return Promise.delay(1000).then(() => w.dmesgAsync())
|
|
|
|
}).catch(e => {
|
2019-09-27 15:53:26 +02:00
|
|
|
pxt.tickEvent("bluetooth.fail");
|
|
|
|
useHID = false;
|
|
|
|
useWebSerial = false;
|
|
|
|
// if we failed to initalize, tell the user to retry
|
|
|
|
return Promise.reject(e)
|
2018-01-04 01:18:39 +01:00
|
|
|
})
|
|
|
|
}
|