/// /// import UF2 = pxtc.UF2; import { Ev3Wrapper } from "./wrap"; export let ev3: Ev3Wrapper; let confirmAsync: (options: any) => Promise; export function setConfirmAsync(cf: (options: any) => Promise) { confirmAsync = cf; } export function debug() { 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; type SerialPortRequestOptions = any; declare class SerialPort { open(options?: SerialOptions): Promise; close(): void; readonly readable: any; readonly writable: any; //getInfo(): SerialPortInfo; } declare interface Serial extends EventTarget { onconnect: any; ondisconnect: any; getPorts(): Promise requestPort(options: SerialPortRequestOptions): Promise; } 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; private _reader: any; private _writer: any; constructor(private port: SerialPort, private options: SerialOptions) { this.openAsync(); } async readSerialAsync() { this._reader = this.port.readable.getReader(); let buffer: Uint8Array; const reader = this._reader; while (reader === this._reader) { // will change if we recycle the connection 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 !!(navigator).serial; } static async mkPacketIOAsync(): Promise { const serial = (navigator).serial; if (serial) { try { const requestOptions: SerialPortRequestOptions = {}; const port = await serial.requestPort(requestOptions); const options: SerialOptions = { baudrate: 460800, buffersize: 4096 }; 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)) } 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(); }); } private closeAsync() { console.log(`serial: closing port`); this.port.close(); this._reader = undefined; this._writer = undefined; return Promise.delay(500); } reconnectAsync(): Promise { return this.openAsync(); } disconnectAsync(): Promise { return this.closeAsync(); } sendPacketAsync(pkt: Uint8Array): Promise { if (!this._writer) this._writer = this.port.writable.getWriter(); return this._writer.write(pkt); } } function hf2Async() { const pktIOAsync: Promise = 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 { if (pxt.U.isNodeJS) { // doesn't seem to work ATM useHID = false } else { const nodehid = /nodehid/i.test(window.location.href); if (pxt.Cloud.isLocalHost() && pxt.Cloud.localToken && nodehid) useHID = true; } if (WebSerialPackageIO.isSupported()) pxt.tickEvent("bluetooth.supported"); return Promise.resolve(); } export function canUseWebSerial() { return WebSerialPackageIO.isSupported(); } export function enableWebSerialAsync() { initPromise = undefined; useWebSerial = WebSerialPackageIO.isSupported(); useHID = useWebSerial; if (useWebSerial) 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 function initHidAsync() { // needs to run within a click handler if (initPromise) return initPromise if (useHID) { initPromise = cleanupAsync() .then(() => hf2Async()) .catch(err => { console.error(err); initPromise = null useHID = false; useWebSerial = false; return Promise.reject(err); }) } else { useHID = false useWebSerial = false; initPromise = Promise.reject(new Error("no HID")) } return initPromise; } // this comes from aux/pxt.lms const fspath = "../prjs/BrkProg_SAVE/" const rbfTemplate = ` 4c45474f580000006d000100000000001c000000000000000e000000821b038405018130813e8053 74617274696e672e2e2e0084006080XX00448581644886488405018130813e80427965210084000a ` export function deployCoreAsync(resp: pxtc.CompileResult) { 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) let origElfUF2 = UF2.parseFile(pxt.U.stringToUint8Array(ts.pxtc.decodeBase64(resp.outfiles[pxt.outputName()]))) 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 = () => { if (pxt.commands && pxt.commands.electronDeployAsync) { return pxt.commands.electronDeployAsync(resp); } if (pxt.commands && pxt.commands.saveOnlyAsync) { return pxt.commands.saveOnlyAsync(resp); } return Promise.resolve(); } if (!useHID) return saveUF2Async() 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.reconnectAsync(false) .catch(e => { // user easily forgets to stop robot if (confirmAsync) return confirmAsync({ header: lf("Bluetooth download failed..."), htmlBody: `
  • ${lf("Make sure to stop your program on the EV3.")}
  • ${lf("Check your battery level.")}
`, 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.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 => { pxt.tickEvent("bluetooth.fail"); useHID = false; useWebSerial = false; // if we failed to initalize, tell the user to retry return Promise.reject(e) }) }