diff --git a/extensions/src/doodlebot/Doodlebot.ts b/extensions/src/doodlebot/Doodlebot.ts index a08638916..3b0c94a15 100644 --- a/extensions/src/doodlebot/Doodlebot.ts +++ b/extensions/src/doodlebot/Doodlebot.ts @@ -69,6 +69,12 @@ const msg = (content: string, type: "success" | "warning" | "error") => { } } +type BLECommunication = { + onDisconnect: (...callbacks: (() => void)[]) => void, + onReceive: (callback: (text: CustomEvent) => void) => void, + send: (text: string) => Promise, +} + export default class Doodlebot { /** * @@ -136,7 +142,11 @@ export default class Doodlebot { { requestBluetooth, credentials, saveIP }: CreatePayload, ...filters: BluetoothLEScanFilter[]) { const { robot, services } = await Doodlebot.getBLE(ble, ...filters); - return new Doodlebot(robot, services, requestBluetooth, credentials, saveIP); + return new Doodlebot({ + onReceive: (callback) => services.uartService.addEventListener("receiveText", callback), + onDisconnect: (callback) => ble.addEventListener("gattserverdisconnected", callback), + send: (text) => services.uartService.sendText(text), + }, requestBluetooth, credentials, saveIP); } private pending: Pending = { motor: undefined, wifi: undefined, websocket: undefined, ip: undefined }; @@ -183,28 +193,16 @@ export default class Doodlebot { private audioCallbacks = new Set<(chunk: Float32Array) => void>(); constructor( - private device: BluetoothDevice, - private services: Services, + private ble: BLECommunication, private requestBluetooth: RequestBluetooth, private credentials: NetworkCredentials, private saveIP: SaveIP ) { - this.attachToBLE(device, services); + this.ble.onReceive(this.receiveTextBLE.bind(this)); + this.ble.onDisconnect(this.handleBleDisconnect.bind(this)); this.connectionWorkflow(credentials); } - private attachToBLE(device: BluetoothDevice, services: Services) { - this.device = device; - this.services = services; - this.subscribe(services.uartService, "receiveText", this.receiveTextBLE.bind(this)); - this.subscribe(device, "gattserverdisconnected", this.handleBleDisconnect.bind(this)); - } - - private subscribe(target: T, event: Subscription["event"], listener: Subscription["listener"]) { - target.addEventListener(event, listener); - this.subscriptions.push({ target, event, listener }); - } - private formCommand(...args: (string | number)[]) { return `(${args.join(",")})`; } @@ -497,38 +495,34 @@ export default class Doodlebot { msg("Could not retrieve IP address from doodlebot", "error") } - return new Promise(async (resolve) => { - const self = this; - const { device } = this; - - //let interval: NodeJS.Timeout; - - const reconnectToBluetooth = async () => { - this.requestBluetooth(async (ble) => { - msg("Reconnected to doodlebot", "success"); - //clearInterval(interval); - const { robot, services } = await Doodlebot.getBLE(ble); - self.attachToBLE(robot, services); - device.removeEventListener("gattserverdisconnected", reconnectToBluetooth); - msg("Waiting to issue connect command", "warning"); - await new Promise((resolve) => setTimeout(resolve, 5000)); - msg("Testing doodlebot's IP after reconnect", "warning"); - const ip = await self.getIPAddress(); - msg( - ip === localIp ? "Doodlebot's IP is local, not valid" : "Doodlebot's IP is valid", - ip === localIp ? "warning" : "success" - ) - resolve(this.setIP(ip)); - }); - } + // return new Promise(async (resolve) => { + // const self = this; + // const { device } = this; - device.addEventListener("gattserverdisconnected", reconnectToBluetooth); + // const reconnectToBluetooth = async () => { + // this.requestBluetooth(async (ble) => { + // msg("Reconnected to doodlebot", "success"); + // const { robot, services } = await Doodlebot.getBLE(ble); + // self.attachToBLE(robot, services); + // device.removeEventListener("gattserverdisconnected", reconnectToBluetooth); + // msg("Waiting to issue connect command", "warning"); + // await new Promise((resolve) => setTimeout(resolve, 5000)); + // msg("Testing doodlebot's IP after reconnect", "warning"); + // const ip = await self.getIPAddress(); + // msg( + // ip === localIp ? "Doodlebot's IP is local, not valid" : "Doodlebot's IP is valid", + // ip === localIp ? "warning" : "success" + // ) + // resolve(this.setIP(ip)); + // }); + // } - msg("Attempting to connect to wifi", "warning"); + // device.addEventListener("gattserverdisconnected", reconnectToBluetooth); - await this.sendBLECommand(command.wifi, credentials.ssid, credentials.password); - //interval = setInterval(() => this.sendBLECommand(command.network), 5000); - }); + // msg("Attempting to connect to wifi", "warning"); + + // await this.sendBLECommand(command.wifi, credentials.ssid, credentials.password); + // }); } /** @@ -635,7 +629,7 @@ export default class Doodlebot { line.unshift(...newSegment); return line; } - + printLine() { console.log(this.wholeString); } @@ -650,7 +644,7 @@ export default class Doodlebot { line; lineCounter = 0; detector; - + async followLine() { let first = true; const delay = 0.5; @@ -660,7 +654,7 @@ export default class Doodlebot { let prevAngle; let lineData await this.detector.initialize(this); - + while (true) { console.log("NEXT"); try { @@ -712,11 +706,11 @@ export default class Doodlebot { const { radius, angle } = command; console.log(command); if (command.distance > 0) { - this.sendWebsocketCommand("m", Math.round(12335.6*command.distance), Math.round(12335.6*command.distance), 500, 500); + this.sendWebsocketCommand("m", Math.round(12335.6 * command.distance), Math.round(12335.6 * command.distance), 500, 500); } else { this.sendBLECommand("t", radius, angle); } - + } console.log("after 2"); console.log(this.cumulativeLine); @@ -828,8 +822,7 @@ export default class Doodlebot { * @returns */ sendBLECommand(command: Command, ...args: (string | number)[]) { - const { uartService } = this.services; - return uartService.sendText(this.formCommand(command, ...args)); + return this.ble.send(this.formCommand(command, ...args)); } /** diff --git a/extensions/src/doodlebot/index.ts b/extensions/src/doodlebot/index.ts index 37f1cf6b2..d6c75db43 100644 --- a/extensions/src/doodlebot/index.ts +++ b/extensions/src/doodlebot/index.ts @@ -81,11 +81,61 @@ export default class DoodlebotBlocks extends extension(details, "ui", "indicator DIMENSIONS = [480, 360]; init(env: Environment) { - this.openUI("Connect"); this.setIndicator("disconnected"); + if (window.isSecureContext) this.openUI("Connect") + else this.connectToDoodlebotWithExternalBLE(); this._loop(); } + private async connectToDoodlebotWithExternalBLE() { + const disconnectMessage = "disconnected"; + const urlParams = new URLSearchParams(window.location.search); // Hack for now + let source: MessageEventSource; + let targetOrigin: string; + + await new Promise((resolve) => { + const onInitialMessage = (event: MessageEvent) => { + source = event.source; + targetOrigin = event.origin; + window.removeEventListener("message", onInitialMessage); + source.postMessage("ready", { targetOrigin }) + resolve(); + } + window.addEventListener("message", onInitialMessage); + }); + + const doodlebot = new Doodlebot( + { + onDisconnect: () => { + window.addEventListener("message", (event) => { + if (event.data !== disconnectMessage) return; + this.setIndicator("disconnected"); + alert("Disconnected from robot"); // Decide how to handle (maybe direct user to close window and go back to https) + }); + }, + onReceive: (callback) => { + window.addEventListener('message', (event) => { + if (event.data === disconnectMessage) return; + callback(event.data); + }); + }, + send: (text) => new Promise(resolve => { + const onMessageReturn = ({ data }: MessageEvent) => { + if (data !== text) return; + window.removeEventListener("message", onMessageReturn); + resolve(); + } + window.addEventListener("message", onMessageReturn); + source.postMessage(text, { targetOrigin }); + }) + }, + () => alert("requestBluetooth called"), // placeholder + { ssid: urlParams.get("ssid"), password: urlParams.get("password"), ipOverride: urlParams.get("ip") }, + () => alert("save IP called"), // placeholder + ) + this.setDoodlebot(doodlebot); + } + setDoodlebot(doodlebot: Doodlebot) { this.doodlebot = doodlebot; this.setIndicator("connected"); @@ -569,7 +619,7 @@ export default class DoodlebotBlocks extends extension(details, "ui", "indicator } @block({ - type: "command", + type: "command", text: (url) => `import model ${url}`, arg: { type: "string", @@ -580,13 +630,13 @@ export default class DoodlebotBlocks extends extension(details, "ui", "indicator await this.useModel(url); } - + @block({ type: "hat", text: (className) => `when model detects ${className}`, arg: { type: "string", - options: function() { + options: function () { if (!this) { throw new Error('Context is undefined'); } @@ -607,7 +657,7 @@ export default class DoodlebotBlocks extends extension(details, "ui", "indicator return this.getModelPrediction(); } - + async useModel(url: string) { try { const modelUrl = this.modelArgumentToURL(url);