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:
		@@ -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)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
									
								
							
							
						
						
									
										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**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 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.
 | 
					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
									
								
							
							
						
						
									
										
											BIN
										
									
								
								docs/static/bluetooth/experimental.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 26 KiB  | 
							
								
								
									
										219
									
								
								editor/deploy.ts
									
									
									
									
									
								
							
							
						
						
									
										219
									
								
								editor/deploy.ts
									
									
									
									
									
								
							@@ -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))))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function hf2Async() {
 | 
					
 | 
				
			||||||
    return pxt.HF2.mkPacketIOAsync()
 | 
					// Web Serial API https://wicg.github.io/serial/
 | 
				
			||||||
        .then(h => {
 | 
					// chromium bug https://bugs.chromium.org/p/chromium/issues/detail?id=884928
 | 
				
			||||||
            let w = new pxt.editor.Ev3Wrapper(h)
 | 
					// Under experimental features in Chrome Desktop 77+
 | 
				
			||||||
            ev3 = w
 | 
					enum ParityType {
 | 
				
			||||||
            return w.reconnectAsync(true)
 | 
					    "none",
 | 
				
			||||||
                .then(() => w)
 | 
					    "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>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let noHID = false
 | 
					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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let initPromise: Promise<pxt.editor.Ev3Wrapper>
 | 
					    constructor(private port: SerialPort, private options: SerialOptions) {
 | 
				
			||||||
export function initAsync() {
 | 
					 | 
				
			||||||
    if (initPromise)
 | 
					 | 
				
			||||||
        return initPromise
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let canHID = false
 | 
					        // 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() {
 | 
				
			||||||
 | 
					    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> {
 | 
				
			||||||
    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)
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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",
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										562
									
								
								editor/wrap.ts
									
									
									
									
									
								
							
							
						
						
									
										562
									
								
								editor/wrap.ts
									
									
									
									
									
								
							@@ -1,285 +1,281 @@
 | 
				
			|||||||
namespace pxt.editor {
 | 
					import HF2 = pxt.HF2
 | 
				
			||||||
    import HF2 = pxt.HF2
 | 
					import U = pxt.U
 | 
				
			||||||
    import U = pxt.U
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function log(msg: string) {
 | 
					 | 
				
			||||||
        pxt.log("EWRAP: " + msg)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    export interface DirEntry {
 | 
					 | 
				
			||||||
        name: string;
 | 
					 | 
				
			||||||
        md5?: string;
 | 
					 | 
				
			||||||
        size?: number;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const runTemplate = "C00882010084XX0060640301606400"
 | 
					 | 
				
			||||||
    const usbMagic = 0x3d3f
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    export class Ev3Wrapper {
 | 
					 | 
				
			||||||
        msgs = new U.PromiseBuffer<Uint8Array>()
 | 
					 | 
				
			||||||
        private cmdSeq = U.randomUint32() & 0xffff;
 | 
					 | 
				
			||||||
        private lock = new U.PromiseQueue();
 | 
					 | 
				
			||||||
        isStreaming = false;
 | 
					 | 
				
			||||||
        dataDump = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        constructor(public io: pxt.HF2.PacketIO) {
 | 
					 | 
				
			||||||
            io.onData = buf => {
 | 
					 | 
				
			||||||
                buf = buf.slice(0, HF2.read16(buf, 0) + 2)
 | 
					 | 
				
			||||||
                if (HF2.read16(buf, 4) == usbMagic) {
 | 
					 | 
				
			||||||
                    let code = HF2.read16(buf, 6)
 | 
					 | 
				
			||||||
                    let payload = buf.slice(8)
 | 
					 | 
				
			||||||
                    if (code == 1) {
 | 
					 | 
				
			||||||
                        let str = U.uint8ArrayToString(payload)
 | 
					 | 
				
			||||||
                        if (Util.isNodeJS)
 | 
					 | 
				
			||||||
                            console.log("SERIAL: " + str.replace(/\n+$/, ""))
 | 
					 | 
				
			||||||
                        else
 | 
					 | 
				
			||||||
                            window.postMessage({
 | 
					 | 
				
			||||||
                                type: 'serial',
 | 
					 | 
				
			||||||
                                id: 'n/a', // TODO?
 | 
					 | 
				
			||||||
                                data: str
 | 
					 | 
				
			||||||
                            }, "*")
 | 
					 | 
				
			||||||
                    } else
 | 
					 | 
				
			||||||
                        console.log("Magic: " + code + ": " + U.toHex(payload))
 | 
					 | 
				
			||||||
                    return
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                if (this.dataDump)
 | 
					 | 
				
			||||||
                    log("RECV: " + U.toHex(buf))
 | 
					 | 
				
			||||||
                this.msgs.push(buf)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        private allocCore(addSize: number, replyType: number) {
 | 
					 | 
				
			||||||
            let len = 5 + addSize
 | 
					 | 
				
			||||||
            let buf = new Uint8Array(len)
 | 
					 | 
				
			||||||
            HF2.write16(buf, 0, len - 2)  // pktLen
 | 
					 | 
				
			||||||
            HF2.write16(buf, 2, this.cmdSeq++) // msgCount
 | 
					 | 
				
			||||||
            buf[4] = replyType
 | 
					 | 
				
			||||||
            return buf
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        private allocSystem(addSize: number, cmd: number, replyType = 1) {
 | 
					 | 
				
			||||||
            let buf = this.allocCore(addSize + 1, replyType)
 | 
					 | 
				
			||||||
            buf[5] = cmd
 | 
					 | 
				
			||||||
            return buf
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        private allocCustom(code: number, addSize = 0) {
 | 
					 | 
				
			||||||
            let buf = this.allocCore(1 + 2 + addSize, 0)
 | 
					 | 
				
			||||||
            HF2.write16(buf, 4, usbMagic)
 | 
					 | 
				
			||||||
            HF2.write16(buf, 6, code)
 | 
					 | 
				
			||||||
            return buf
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        stopAsync() {
 | 
					 | 
				
			||||||
            return this.isVmAsync()
 | 
					 | 
				
			||||||
                .then(vm => {
 | 
					 | 
				
			||||||
                    if (vm) return Promise.resolve();
 | 
					 | 
				
			||||||
                    log(`stopping PXT app`)
 | 
					 | 
				
			||||||
                    let buf = this.allocCustom(2)
 | 
					 | 
				
			||||||
                    return this.justSendAsync(buf)
 | 
					 | 
				
			||||||
                        .then(() => Promise.delay(500))
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        dmesgAsync() {
 | 
					 | 
				
			||||||
            log(`asking for DMESG buffer over serial`)
 | 
					 | 
				
			||||||
            let buf = this.allocCustom(3)
 | 
					 | 
				
			||||||
            return this.justSendAsync(buf)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        runAsync(path: string) {
 | 
					 | 
				
			||||||
            let codeHex = runTemplate.replace("XX", U.toHex(U.stringToUint8Array(path)))
 | 
					 | 
				
			||||||
            let code = U.fromHex(codeHex)
 | 
					 | 
				
			||||||
            let pkt = this.allocCore(2 + code.length, 0)
 | 
					 | 
				
			||||||
            HF2.write16(pkt, 5, 0x0800)
 | 
					 | 
				
			||||||
            U.memcpy(pkt, 7, code)
 | 
					 | 
				
			||||||
            log(`run ${path}`)
 | 
					 | 
				
			||||||
            return this.justSendAsync(pkt)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        justSendAsync(buf: Uint8Array) {
 | 
					 | 
				
			||||||
            return this.lock.enqueue("talk", () => {
 | 
					 | 
				
			||||||
                this.msgs.drain()
 | 
					 | 
				
			||||||
                if (this.dataDump)
 | 
					 | 
				
			||||||
                    log("SEND: " + U.toHex(buf))
 | 
					 | 
				
			||||||
                return this.io.sendPacketAsync(buf)
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        talkAsync(buf: Uint8Array, altResponse = 0) {
 | 
					 | 
				
			||||||
            return this.lock.enqueue("talk", () => {
 | 
					 | 
				
			||||||
                this.msgs.drain()
 | 
					 | 
				
			||||||
                if (this.dataDump)
 | 
					 | 
				
			||||||
                    log("TALK: " + U.toHex(buf))
 | 
					 | 
				
			||||||
                return this.io.sendPacketAsync(buf)
 | 
					 | 
				
			||||||
                    .then(() => this.msgs.shiftAsync(1000))
 | 
					 | 
				
			||||||
                    .then(resp => {
 | 
					 | 
				
			||||||
                        if (resp[2] != buf[2] || resp[3] != buf[3])
 | 
					 | 
				
			||||||
                            U.userError("msg count de-sync")
 | 
					 | 
				
			||||||
                        if (buf[4] == 1) {
 | 
					 | 
				
			||||||
                            if (altResponse != -1 && resp[5] != buf[5])
 | 
					 | 
				
			||||||
                                U.userError("cmd de-sync")
 | 
					 | 
				
			||||||
                            if (altResponse != -1 && resp[6] != 0 && resp[6] != altResponse)
 | 
					 | 
				
			||||||
                                U.userError("cmd error: " + resp[6])
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        return resp
 | 
					 | 
				
			||||||
                    })
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        flashAsync(path: string, file: Uint8Array) {
 | 
					 | 
				
			||||||
            log(`write ${file.length} bytes to ${path}`)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            let handle = -1
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            let loopAsync = (pos: number): Promise<void> => {
 | 
					 | 
				
			||||||
                if (pos >= file.length) return Promise.resolve()
 | 
					 | 
				
			||||||
                let size = file.length - pos
 | 
					 | 
				
			||||||
                if (size > 1000) size = 1000
 | 
					 | 
				
			||||||
                let upl = this.allocSystem(1 + size, 0x93, 0x1)
 | 
					 | 
				
			||||||
                upl[6] = handle
 | 
					 | 
				
			||||||
                U.memcpy(upl, 6 + 1, file, pos, size)
 | 
					 | 
				
			||||||
                return this.talkAsync(upl, 8) // 8=EOF
 | 
					 | 
				
			||||||
                    .then(() => loopAsync(pos + size))
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            let begin = this.allocSystem(4 + path.length + 1, 0x92)
 | 
					 | 
				
			||||||
            HF2.write32(begin, 6, file.length) // fileSize
 | 
					 | 
				
			||||||
            U.memcpy(begin, 10, U.stringToUint8Array(path))
 | 
					 | 
				
			||||||
            return this.lock.enqueue("file", () =>
 | 
					 | 
				
			||||||
                this.talkAsync(begin)
 | 
					 | 
				
			||||||
                    .then(resp => {
 | 
					 | 
				
			||||||
                        handle = resp[7]
 | 
					 | 
				
			||||||
                        return loopAsync(0)
 | 
					 | 
				
			||||||
                    }))
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        lsAsync(path: string): Promise<DirEntry[]> {
 | 
					 | 
				
			||||||
            let lsReq = this.allocSystem(2 + path.length + 1, 0x99)
 | 
					 | 
				
			||||||
            HF2.write16(lsReq, 6, 1024) // maxRead
 | 
					 | 
				
			||||||
            U.memcpy(lsReq, 8, U.stringToUint8Array(path))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return this.talkAsync(lsReq, 8)
 | 
					 | 
				
			||||||
                .then(resp =>
 | 
					 | 
				
			||||||
                    U.uint8ArrayToString(resp.slice(12)).split(/\n/).map(s => {
 | 
					 | 
				
			||||||
                        if (!s) return null as DirEntry
 | 
					 | 
				
			||||||
                        let m = /^([A-F0-9]+) ([A-F0-9]+) ([^\/]*)$/.exec(s)
 | 
					 | 
				
			||||||
                        if (m)
 | 
					 | 
				
			||||||
                            return {
 | 
					 | 
				
			||||||
                                md5: m[1],
 | 
					 | 
				
			||||||
                                size: parseInt(m[2], 16),
 | 
					 | 
				
			||||||
                                name: m[3]
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        else
 | 
					 | 
				
			||||||
                            return {
 | 
					 | 
				
			||||||
                                name: s.replace(/\/$/, "")
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                    }).filter(v => !!v))
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        rmAsync(path: string): Promise<void> {
 | 
					 | 
				
			||||||
            log(`rm ${path}`)
 | 
					 | 
				
			||||||
            let rmReq = this.allocSystem(path.length + 1, 0x9c)
 | 
					 | 
				
			||||||
            U.memcpy(rmReq, 6, U.stringToUint8Array(path))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return this.talkAsync(rmReq, 5)
 | 
					 | 
				
			||||||
                .then(resp => { })
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        isVmAsync(): Promise<boolean> {
 | 
					 | 
				
			||||||
            let path = "/no/such/dir"
 | 
					 | 
				
			||||||
            let mkdirReq = this.allocSystem(path.length + 1, 0x9b)
 | 
					 | 
				
			||||||
            U.memcpy(mkdirReq, 6, U.stringToUint8Array(path))
 | 
					 | 
				
			||||||
            return this.talkAsync(mkdirReq, -1)
 | 
					 | 
				
			||||||
                .then(resp => {
 | 
					 | 
				
			||||||
                    let isVM = resp[6] == 0x05
 | 
					 | 
				
			||||||
                    log(`${isVM ? "PXT app" : "VM"} running`)
 | 
					 | 
				
			||||||
                    return isVM
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        private streamFileOnceAsync(path: string, cb: (d: Uint8Array) => void) {
 | 
					 | 
				
			||||||
            let fileSize = 0
 | 
					 | 
				
			||||||
            let filePtr = 0
 | 
					 | 
				
			||||||
            let handle = -1
 | 
					 | 
				
			||||||
            let resp = (buf: Uint8Array): Promise<void> => {
 | 
					 | 
				
			||||||
                if (buf[6] == 2) {
 | 
					 | 
				
			||||||
                    // handle not ready - file is missing
 | 
					 | 
				
			||||||
                    this.isStreaming = false
 | 
					 | 
				
			||||||
                    return Promise.resolve()
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if (buf[6] != 0 && buf[6] != 8)
 | 
					 | 
				
			||||||
                    U.userError("bad response when streaming file: " + buf[6] + " " + U.toHex(buf))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                this.isStreaming = true
 | 
					 | 
				
			||||||
                fileSize = HF2.read32(buf, 7)
 | 
					 | 
				
			||||||
                if (handle == -1) {
 | 
					 | 
				
			||||||
                    handle = buf[11]
 | 
					 | 
				
			||||||
                    log(`stream on, handle=${handle}`)
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                let data = buf.slice(12)
 | 
					 | 
				
			||||||
                filePtr += data.length
 | 
					 | 
				
			||||||
                if (data.length > 0)
 | 
					 | 
				
			||||||
                    cb(data)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if (buf[6] == 8) {
 | 
					 | 
				
			||||||
                    // end of file
 | 
					 | 
				
			||||||
                    this.isStreaming = false
 | 
					 | 
				
			||||||
                    return this.rmAsync(path)
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                let contFileReq = this.allocSystem(1 + 2, 0x97)
 | 
					 | 
				
			||||||
                HF2.write16(contFileReq, 7, 1000) // maxRead
 | 
					 | 
				
			||||||
                contFileReq[6] = handle
 | 
					 | 
				
			||||||
                return Promise.delay(data.length > 0 ? 0 : 500)
 | 
					 | 
				
			||||||
                    .then(() => this.talkAsync(contFileReq, -1))
 | 
					 | 
				
			||||||
                    .then(resp)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            let getFileReq = this.allocSystem(2 + path.length + 1, 0x96)
 | 
					 | 
				
			||||||
            HF2.write16(getFileReq, 6, 1000) // maxRead
 | 
					 | 
				
			||||||
            U.memcpy(getFileReq, 8, U.stringToUint8Array(path))
 | 
					 | 
				
			||||||
            return this.talkAsync(getFileReq, -1).then(resp)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        streamFileAsync(path: string, cb: (d: Uint8Array) => void) {
 | 
					 | 
				
			||||||
            let loop = (): Promise<void> =>
 | 
					 | 
				
			||||||
                this.lock.enqueue("file", () =>
 | 
					 | 
				
			||||||
                    this.streamFileOnceAsync(path, cb))
 | 
					 | 
				
			||||||
                    .then(() => Promise.delay(500))
 | 
					 | 
				
			||||||
                    .then(loop)
 | 
					 | 
				
			||||||
            return loop()
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        downloadFileAsync(path: string, cb: (d: Uint8Array) => void) {
 | 
					 | 
				
			||||||
            return this.lock.enqueue("file", () =>
 | 
					 | 
				
			||||||
                    this.streamFileOnceAsync(path, cb))
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        private initAsync() {
 | 
					 | 
				
			||||||
            return Promise.resolve()
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        private resetState() {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        reconnectAsync(first = false): Promise<void> {
 | 
					 | 
				
			||||||
            this.resetState()
 | 
					 | 
				
			||||||
            if (first) return this.initAsync()
 | 
					 | 
				
			||||||
            log(`reconnect`);
 | 
					 | 
				
			||||||
            return this.io.reconnectAsync()
 | 
					 | 
				
			||||||
                .then(() => this.initAsync())
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        disconnectAsync() {
 | 
					 | 
				
			||||||
            log(`disconnect`);
 | 
					 | 
				
			||||||
            return this.io.disconnectAsync()
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function log(msg: string) {
 | 
				
			||||||
 | 
					    pxt.debug("EWRAP: " + msg)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface DirEntry {
 | 
				
			||||||
 | 
					    name: string;
 | 
				
			||||||
 | 
					    md5?: string;
 | 
				
			||||||
 | 
					    size?: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const runTemplate = "C00882010084XX0060640301606400"
 | 
				
			||||||
 | 
					const usbMagic = 0x3d3f
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class Ev3Wrapper {
 | 
				
			||||||
 | 
					    msgs = new U.PromiseBuffer<Uint8Array>()
 | 
				
			||||||
 | 
					    private cmdSeq = U.randomUint32() & 0xffff;
 | 
				
			||||||
 | 
					    private lock = new U.PromiseQueue();
 | 
				
			||||||
 | 
					    isStreaming = false;
 | 
				
			||||||
 | 
					    dataDump = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructor(public io: pxt.HF2.PacketIO) {
 | 
				
			||||||
 | 
					        io.onData = buf => {
 | 
				
			||||||
 | 
					            buf = buf.slice(0, HF2.read16(buf, 0) + 2)
 | 
				
			||||||
 | 
					            if (HF2.read16(buf, 4) == usbMagic) {
 | 
				
			||||||
 | 
					                let code = HF2.read16(buf, 6)
 | 
				
			||||||
 | 
					                let payload = buf.slice(8)
 | 
				
			||||||
 | 
					                if (code == 1) {
 | 
				
			||||||
 | 
					                    let str = U.uint8ArrayToString(payload)
 | 
				
			||||||
 | 
					                    if (U.isNodeJS)
 | 
				
			||||||
 | 
					                        pxt.debug("SERIAL: " + str.replace(/\n+$/, ""))
 | 
				
			||||||
 | 
					                    else
 | 
				
			||||||
 | 
					                        window.postMessage({
 | 
				
			||||||
 | 
					                            type: 'serial',
 | 
				
			||||||
 | 
					                            id: 'n/a', // TODO?
 | 
				
			||||||
 | 
					                            data: str
 | 
				
			||||||
 | 
					                        }, "*")
 | 
				
			||||||
 | 
					                } else
 | 
				
			||||||
 | 
					                    pxt.debug("Magic: " + code + ": " + U.toHex(payload))
 | 
				
			||||||
 | 
					                return
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (this.dataDump)
 | 
				
			||||||
 | 
					                log("RECV: " + U.toHex(buf))
 | 
				
			||||||
 | 
					            this.msgs.push(buf)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private allocCore(addSize: number, replyType: number) {
 | 
				
			||||||
 | 
					        let len = 5 + addSize
 | 
				
			||||||
 | 
					        let buf = new Uint8Array(len)
 | 
				
			||||||
 | 
					        HF2.write16(buf, 0, len - 2)  // pktLen
 | 
				
			||||||
 | 
					        HF2.write16(buf, 2, this.cmdSeq++) // msgCount
 | 
				
			||||||
 | 
					        buf[4] = replyType
 | 
				
			||||||
 | 
					        return buf
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private allocSystem(addSize: number, cmd: number, replyType = 1) {
 | 
				
			||||||
 | 
					        let buf = this.allocCore(addSize + 1, replyType)
 | 
				
			||||||
 | 
					        buf[5] = cmd
 | 
				
			||||||
 | 
					        return buf
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private allocCustom(code: number, addSize = 0) {
 | 
				
			||||||
 | 
					        let buf = this.allocCore(1 + 2 + addSize, 0)
 | 
				
			||||||
 | 
					        HF2.write16(buf, 4, usbMagic)
 | 
				
			||||||
 | 
					        HF2.write16(buf, 6, code)
 | 
				
			||||||
 | 
					        return buf
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    stopAsync() {
 | 
				
			||||||
 | 
					        return this.isVmAsync()
 | 
				
			||||||
 | 
					            .then(vm => {
 | 
				
			||||||
 | 
					                if (vm) return Promise.resolve();
 | 
				
			||||||
 | 
					                log(`stopping PXT app`)
 | 
				
			||||||
 | 
					                let buf = this.allocCustom(2)
 | 
				
			||||||
 | 
					                return this.justSendAsync(buf)
 | 
				
			||||||
 | 
					                    .then(() => Promise.delay(500))
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dmesgAsync() {
 | 
				
			||||||
 | 
					        log(`asking for DMESG buffer over serial`)
 | 
				
			||||||
 | 
					        let buf = this.allocCustom(3)
 | 
				
			||||||
 | 
					        return this.justSendAsync(buf)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    runAsync(path: string) {
 | 
				
			||||||
 | 
					        let codeHex = runTemplate.replace("XX", U.toHex(U.stringToUint8Array(path)))
 | 
				
			||||||
 | 
					        let code = U.fromHex(codeHex)
 | 
				
			||||||
 | 
					        let pkt = this.allocCore(2 + code.length, 0)
 | 
				
			||||||
 | 
					        HF2.write16(pkt, 5, 0x0800)
 | 
				
			||||||
 | 
					        U.memcpy(pkt, 7, code)
 | 
				
			||||||
 | 
					        log(`run ${path}`)
 | 
				
			||||||
 | 
					        return this.justSendAsync(pkt)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    justSendAsync(buf: Uint8Array) {
 | 
				
			||||||
 | 
					        return this.lock.enqueue("talk", () => {
 | 
				
			||||||
 | 
					            this.msgs.drain()
 | 
				
			||||||
 | 
					            if (this.dataDump)
 | 
				
			||||||
 | 
					                log("SEND: " + U.toHex(buf))
 | 
				
			||||||
 | 
					            return this.io.sendPacketAsync(buf)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    talkAsync(buf: Uint8Array, altResponse = 0) {
 | 
				
			||||||
 | 
					        return this.lock.enqueue("talk", () => {
 | 
				
			||||||
 | 
					            this.msgs.drain()
 | 
				
			||||||
 | 
					            if (this.dataDump)
 | 
				
			||||||
 | 
					                log("TALK: " + U.toHex(buf))
 | 
				
			||||||
 | 
					            return this.io.sendPacketAsync(buf)
 | 
				
			||||||
 | 
					                .then(() => this.msgs.shiftAsync(1000))
 | 
				
			||||||
 | 
					                .then(resp => {
 | 
				
			||||||
 | 
					                    if (resp[2] != buf[2] || resp[3] != buf[3])
 | 
				
			||||||
 | 
					                        U.userError("msg count de-sync")
 | 
				
			||||||
 | 
					                    if (buf[4] == 1) {
 | 
				
			||||||
 | 
					                        if (altResponse != -1 && resp[5] != buf[5])
 | 
				
			||||||
 | 
					                            U.userError("cmd de-sync")
 | 
				
			||||||
 | 
					                        if (altResponse != -1 && resp[6] != 0 && resp[6] != altResponse)
 | 
				
			||||||
 | 
					                            U.userError("cmd error: " + resp[6])
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    return resp
 | 
				
			||||||
 | 
					                })
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    flashAsync(path: string, file: Uint8Array) {
 | 
				
			||||||
 | 
					        log(`write ${file.length} bytes to ${path}`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let handle = -1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let loopAsync = (pos: number): Promise<void> => {
 | 
				
			||||||
 | 
					            if (pos >= file.length) return Promise.resolve()
 | 
				
			||||||
 | 
					            let size = file.length - pos
 | 
				
			||||||
 | 
					            if (size > 1000) size = 1000
 | 
				
			||||||
 | 
					            let upl = this.allocSystem(1 + size, 0x93, 0x1)
 | 
				
			||||||
 | 
					            upl[6] = handle
 | 
				
			||||||
 | 
					            U.memcpy(upl, 6 + 1, file, pos, size)
 | 
				
			||||||
 | 
					            return this.talkAsync(upl, 8) // 8=EOF
 | 
				
			||||||
 | 
					                .then(() => loopAsync(pos + size))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let begin = this.allocSystem(4 + path.length + 1, 0x92)
 | 
				
			||||||
 | 
					        HF2.write32(begin, 6, file.length) // fileSize
 | 
				
			||||||
 | 
					        U.memcpy(begin, 10, U.stringToUint8Array(path))
 | 
				
			||||||
 | 
					        return this.lock.enqueue("file", () =>
 | 
				
			||||||
 | 
					            this.talkAsync(begin)
 | 
				
			||||||
 | 
					                .then(resp => {
 | 
				
			||||||
 | 
					                    handle = resp[7]
 | 
				
			||||||
 | 
					                    return loopAsync(0)
 | 
				
			||||||
 | 
					                }))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    lsAsync(path: string): Promise<DirEntry[]> {
 | 
				
			||||||
 | 
					        let lsReq = this.allocSystem(2 + path.length + 1, 0x99)
 | 
				
			||||||
 | 
					        HF2.write16(lsReq, 6, 1024) // maxRead
 | 
				
			||||||
 | 
					        U.memcpy(lsReq, 8, U.stringToUint8Array(path))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return this.talkAsync(lsReq, 8)
 | 
				
			||||||
 | 
					            .then(resp =>
 | 
				
			||||||
 | 
					                U.uint8ArrayToString(resp.slice(12)).split(/\n/).map(s => {
 | 
				
			||||||
 | 
					                    if (!s) return null as DirEntry
 | 
				
			||||||
 | 
					                    let m = /^([A-F0-9]+) ([A-F0-9]+) ([^\/]*)$/.exec(s)
 | 
				
			||||||
 | 
					                    if (m)
 | 
				
			||||||
 | 
					                        return {
 | 
				
			||||||
 | 
					                            md5: m[1],
 | 
				
			||||||
 | 
					                            size: parseInt(m[2], 16),
 | 
				
			||||||
 | 
					                            name: m[3]
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    else
 | 
				
			||||||
 | 
					                        return {
 | 
				
			||||||
 | 
					                            name: s.replace(/\/$/, "")
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                }).filter(v => !!v))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    rmAsync(path: string): Promise<void> {
 | 
				
			||||||
 | 
					        log(`rm ${path}`)
 | 
				
			||||||
 | 
					        let rmReq = this.allocSystem(path.length + 1, 0x9c)
 | 
				
			||||||
 | 
					        U.memcpy(rmReq, 6, U.stringToUint8Array(path))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return this.talkAsync(rmReq, 5)
 | 
				
			||||||
 | 
					            .then(resp => { })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    isVmAsync(): Promise<boolean> {
 | 
				
			||||||
 | 
					        let path = "/no/such/dir"
 | 
				
			||||||
 | 
					        let mkdirReq = this.allocSystem(path.length + 1, 0x9b)
 | 
				
			||||||
 | 
					        U.memcpy(mkdirReq, 6, U.stringToUint8Array(path))
 | 
				
			||||||
 | 
					        return this.talkAsync(mkdirReq, -1)
 | 
				
			||||||
 | 
					            .then(resp => {
 | 
				
			||||||
 | 
					                let isVM = resp[6] == 0x05
 | 
				
			||||||
 | 
					                log(`${isVM ? "PXT app" : "VM"} running`)
 | 
				
			||||||
 | 
					                return isVM
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private streamFileOnceAsync(path: string, cb: (d: Uint8Array) => void) {
 | 
				
			||||||
 | 
					        let fileSize = 0
 | 
				
			||||||
 | 
					        let filePtr = 0
 | 
				
			||||||
 | 
					        let handle = -1
 | 
				
			||||||
 | 
					        let resp = (buf: Uint8Array): Promise<void> => {
 | 
				
			||||||
 | 
					            if (buf[6] == 2) {
 | 
				
			||||||
 | 
					                // handle not ready - file is missing
 | 
				
			||||||
 | 
					                this.isStreaming = false
 | 
				
			||||||
 | 
					                return Promise.resolve()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (buf[6] != 0 && buf[6] != 8)
 | 
				
			||||||
 | 
					                U.userError("bad response when streaming file: " + buf[6] + " " + U.toHex(buf))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            this.isStreaming = true
 | 
				
			||||||
 | 
					            fileSize = HF2.read32(buf, 7)
 | 
				
			||||||
 | 
					            if (handle == -1) {
 | 
				
			||||||
 | 
					                handle = buf[11]
 | 
				
			||||||
 | 
					                log(`stream on, handle=${handle}`)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            let data = buf.slice(12)
 | 
				
			||||||
 | 
					            filePtr += data.length
 | 
				
			||||||
 | 
					            if (data.length > 0)
 | 
				
			||||||
 | 
					                cb(data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (buf[6] == 8) {
 | 
				
			||||||
 | 
					                // end of file
 | 
				
			||||||
 | 
					                this.isStreaming = false
 | 
				
			||||||
 | 
					                return this.rmAsync(path)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let contFileReq = this.allocSystem(1 + 2, 0x97)
 | 
				
			||||||
 | 
					            HF2.write16(contFileReq, 7, 1000) // maxRead
 | 
				
			||||||
 | 
					            contFileReq[6] = handle
 | 
				
			||||||
 | 
					            return Promise.delay(data.length > 0 ? 0 : 500)
 | 
				
			||||||
 | 
					                .then(() => this.talkAsync(contFileReq, -1))
 | 
				
			||||||
 | 
					                .then(resp)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let getFileReq = this.allocSystem(2 + path.length + 1, 0x96)
 | 
				
			||||||
 | 
					        HF2.write16(getFileReq, 6, 1000) // maxRead
 | 
				
			||||||
 | 
					        U.memcpy(getFileReq, 8, U.stringToUint8Array(path))
 | 
				
			||||||
 | 
					        return this.talkAsync(getFileReq, -1).then(resp)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    streamFileAsync(path: string, cb: (d: Uint8Array) => void) {
 | 
				
			||||||
 | 
					        let loop = (): Promise<void> =>
 | 
				
			||||||
 | 
					            this.lock.enqueue("file", () =>
 | 
				
			||||||
 | 
					                this.streamFileOnceAsync(path, cb))
 | 
				
			||||||
 | 
					                .then(() => Promise.delay(500))
 | 
				
			||||||
 | 
					                .then(loop)
 | 
				
			||||||
 | 
					        return loop()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    downloadFileAsync(path: string, cb: (d: Uint8Array) => void) {
 | 
				
			||||||
 | 
					        return this.lock.enqueue("file", () =>
 | 
				
			||||||
 | 
					            this.streamFileOnceAsync(path, cb))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private initAsync() {
 | 
				
			||||||
 | 
					        return Promise.resolve()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private resetState() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    reconnectAsync(first = false): Promise<void> {
 | 
				
			||||||
 | 
					        this.resetState()
 | 
				
			||||||
 | 
					        if (first) return this.initAsync()
 | 
				
			||||||
 | 
					        log(`reconnect`);
 | 
				
			||||||
 | 
					        return this.io.reconnectAsync()
 | 
				
			||||||
 | 
					            .then(() => this.initAsync())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    disconnectAsync() {
 | 
				
			||||||
 | 
					        log(`disconnect`);
 | 
				
			||||||
 | 
					        return this.io.disconnectAsync()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user