@@ -84,7 +74,36 @@ pxt.editor.initExtensionsAsync = function (opts: pxt.editor.ExtensionOptions): P
hideAgree: false,
agreeLbl: lf("I got it"),
className: 'downloaddialog',
- buttons: [downloadAgain ? {
+ buttons: [canUseWebSerial() ? {
+ label: lf("Bluetooth"),
+ icon: "bluetooth",
+ className: "bluetooth focused",
+ onclick: () => {
+ pxt.tickEvent("bluetooth.enable");
+ enableWebSerial();
+ confirmAsync({
+ header: lf("Bluetooth enabled"),
+ hasCloseIcon: true,
+ hideCancel: true,
+ buttons: [{
+ label: lf("Help"),
+ icon: "question circle",
+ className: "lightgrey",
+ url: "/bluetooth"
+ }],
+ htmlBody: `
+
+${lf("Please download again to send your code to the EV3 over Bluetooth.")}
+
+
+${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.")}
+
+`
+ })
+ }
+ } : undefined, downloadAgain ? {
label: fn,
icon: "download",
className: "lightgrey focused",
diff --git a/editor/wrap.ts b/editor/wrap.ts
index d27f6bf1..76fafb8c 100644
--- a/editor/wrap.ts
+++ b/editor/wrap.ts
@@ -1,285 +1,281 @@
-namespace pxt.editor {
- import HF2 = pxt.HF2
- import U = pxt.U
+import HF2 = pxt.HF2
+import U = pxt.U
- function log(msg: string) {
- pxt.log("EWRAP: " + msg)
+function log(msg: string) {
+ pxt.debug("EWRAP: " + msg)
+}
+
+export interface DirEntry {
+ name: string;
+ md5?: string;
+ size?: number;
+}
+
+const runTemplate = "C00882010084XX0060640301606400"
+const usbMagic = 0x3d3f
+
+export class Ev3Wrapper {
+ msgs = new U.PromiseBuffer
()
+ 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)
+ }
}
- export interface DirEntry {
- name: string;
- md5?: string;
- size?: number;
+ 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
}
- const runTemplate = "C00882010084XX0060640301606400"
- const usbMagic = 0x3d3f
+ private allocSystem(addSize: number, cmd: number, replyType = 1) {
+ let buf = this.allocCore(addSize + 1, replyType)
+ buf[5] = cmd
+ return buf
+ }
- export class Ev3Wrapper {
- msgs = new U.PromiseBuffer()
- private cmdSeq = U.randomUint32() & 0xffff;
- private lock = new U.PromiseQueue();
- isStreaming = false;
- dataDump = false;
+ 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
+ }
- 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 => {
- 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 {
- 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 {
- 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 {
- 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 => {
- 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 =>
- this.lock.enqueue("file", () =>
- this.streamFileOnceAsync(path, cb))
+ 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))
- .then(loop)
- return loop()
+ })
+ }
+
+ 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 => {
+ 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)
+ }))
+ }
- downloadFileAsync(path: string, cb: (d: Uint8Array) => void) {
- return this.lock.enqueue("file", () =>
- this.streamFileOnceAsync(path, cb))
- }
-
+ lsAsync(path: string): Promise {
+ let lsReq = this.allocSystem(2 + path.length + 1, 0x99)
+ HF2.write16(lsReq, 6, 1024) // maxRead
+ U.memcpy(lsReq, 8, U.stringToUint8Array(path))
- private initAsync() {
- return Promise.resolve()
+ 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 {
+ 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 {
+ 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 => {
+ 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)
}
- private resetState() {
+ 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)
+ }
- }
-
- reconnectAsync(first = false): Promise {
- this.resetState()
- if (first) return this.initAsync()
- log(`reconnect`);
- return this.io.reconnectAsync()
- .then(() => this.initAsync())
- }
-
- disconnectAsync() {
- log(`disconnect`);
- return this.io.disconnectAsync()
- }
+ streamFileAsync(path: string, cb: (d: Uint8Array) => void) {
+ let loop = (): Promise =>
+ this.lock.enqueue("file", () =>
+ this.streamFileOnceAsync(path, cb))
+ .then(() => Promise.delay(500))
+ .then(loop)
+ return loop()
}
-}
\ No newline at end of file
+ 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 {
+ this.resetState()
+ if (first) return this.initAsync()
+ log(`reconnect`);
+ return this.io.reconnectAsync()
+ .then(() => this.initAsync())
+ }
+
+ disconnectAsync() {
+ log(`disconnect`);
+ return this.io.disconnectAsync()
+ }
+}
diff --git a/theme/style.less b/theme/style.less
index 69a407f7..6598c49e 100644
--- a/theme/style.less
+++ b/theme/style.less
@@ -185,3 +185,8 @@
font-family: 'legoIcons' !important;
content: "\f119" !important;
}
+
+.bluetooth {
+ background-color: #007EF4 !important;
+ color: white !important;
+}
\ No newline at end of file