Skip to content

Commit

Permalink
Added connection UI
Browse files Browse the repository at this point in the history
  • Loading branch information
pmalacho-mit committed Jan 10, 2024
1 parent 23e8c57 commit 48c9700
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 50 deletions.
134 changes: 134 additions & 0 deletions extensions/src/doodlebot/Connect.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<script lang="ts">
import type Extension from ".";
import { ReactiveInvoke, reactiveInvoke, color } from "$common";
import { onMount } from "svelte";
import Doodlebot from "./Doodlebot";
export let extension: Extension;
export let close: () => void;
const { bluetooth } = window.navigator;
const invoke: ReactiveInvoke<Extension> = (functionName, ...args) =>
reactiveInvoke((extension = extension), functionName, args);
let error: string;
let ssid: string;
let password: string;
const inputs = {
ssid: null as HTMLInputElement,
password: null as HTMLInputElement,
};
const createConnection = async () => {
try {
const doodlebot = await Doodlebot.tryCreate(ssid, password, bluetooth);
invoke("setDoodlebot", doodlebot);
close();
} catch (err) {
invoke("setIndicator", "disconnected");
console.error(err);
error =
err.message === "Bluetooth adapter not available."
? "Your device does not support BLE connections."
: err.message == "User cancelled the requestDevice() chooser."
? "You must select a device to connect to. Please try again."
: err.message !== "User cancelled the requestDevice() chooser."
? "There was a problem connecting your device, please try again or request assistance."
: err.message;
}
};
const updateNetworkCredentials = () =>
extension.doodlebot.connectToWebsocket({ ssid, password });
</script>

<div
class="container"
style:width="100%"
style:background-color={color.ui.white}
style:color={color.text.primary}
>
{#if error}
<div class="error">
{error}
</div>
{/if}
{#if bluetooth}
{#if !extension.doodlebot}
<h1>How to connect to doodlebot</h1>
<div>
<h3>1. Set network credentials:</h3>
<p>
SSID (Network Name):
<input
bind:this={inputs.ssid}
bind:value={ssid}
type="text"
placeholder="e.g. my_wifi"
/>
</p>
<p>
Password:
<input
bind:this={inputs.password}
bind:value={password}
type="text"
placeholder="e.g. 12345"
/>
</p>
</div>
<div>
<h3>2. Select bluetooth device</h3>

<button disabled={!password || !ssid} on:click={createConnection}>
Open Bluetooth Menu
</button>
</div>
{:else}
{@const credentials = extension.doodlebot.getNetworkCredentials()}
<h1>Connected to doodlebot</h1>
<div>
<h3>Update network credentials:</h3>
SSID (Network Name):
<input bind:this={inputs.ssid} type="text" value={credentials.ssid} />
Password:
<input
bind:this={inputs.password}
type="text"
value={credentials.password}
/>
<button
disabled={credentials.ssid === ssid &&
credentials.password === password}
on:click={updateNetworkCredentials}
>
Update</button
>
</div>
<div>
<button>Disconnect</button>
</div>
{/if}
{:else}
Uh oh! Your browser does not support bluetooth. Here's how to fix that...
TBD
{/if}
</div>

<style>
.container {
text-align: center;
padding: 30px;
}
.error {
background-color: red;
color: white;
padding: 4px 8px;
text-align: center;
border-radius: 5px;
}
</style>
27 changes: 21 additions & 6 deletions extensions/src/doodlebot/Doodlebot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import UartService from "./communication/UartService";
import { Command, NetworkStatus, ReceivedCommand, SensorKey, command, keyBySensor, motorCommandReceived, networkStatus, port, sensor } from "./enums";

export type Services = Awaited<ReturnType<typeof Doodlebot.getServices>>;
export type MotorStepRequest = { steps: number, stepsPerSecond: number };
export type MotorStepRequest = {
/** Number of steps for the stepper (+ == forward, - == reverse) */
steps: number,
/** Number of steps/second (rate) of stepper */
stepsPerSecond: number
};
export type Bumper = { front: number, back: number };
export type Vector3D = { x: number, y: number, z: number };
export type Color = { red: number, green: number, blue: number, alpha: number };
Expand Down Expand Up @@ -275,13 +280,19 @@ export default class Doodlebot {
return this.sensorData[type];
}

/**
* @typedef {Object} MotorStepRequest
* @property {number} steps - The number of steps the motor should move.
* @property {number} stepsPerSecond - The speed of the motor in steps per second.
*/

/**
*
* @param type
* @param left
* @param rightSteps
* @param left {MotorStepRequest}
* @param right
*/
async motorCommand(type: "steps", left: MotorStepRequest, rightSteps: MotorStepRequest);
async motorCommand(type: "steps", left: MotorStepRequest, right: MotorStepRequest);
/**
*
* @param type
Expand All @@ -294,7 +305,7 @@ export default class Doodlebot {
* @param type
*/
async motorCommand(type: "stop");
async motorCommand(type: string, ...args: any[]) {
async motorCommand(type: MotorCommand, ...args: any[]) {
const { pending: { motor: pending } } = this;
switch (type) {
case "steps": {
Expand Down Expand Up @@ -377,12 +388,16 @@ export default class Doodlebot {
}));
}

async display(type: "clear");
async display(type: "clear"): Promise<void>;
async display(type: DisplayCommand) {
switch (type) {
case "clear":
await this.sendWebsocketCommand(command.display, "c");
break;
}
}

getNetworkCredentials(): NetworkCredentials {
return { ssid: this.ssid, password: this.wifiPassword };
}
}
10 changes: 1 addition & 9 deletions extensions/src/doodlebot/communication/UartService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,7 @@
*/

import EventDispatcher from "./EventDispatcher";
import ServiceHelper, { Service } from "./ServiceHelper";

/**
* Events raised by the UART service
* newListener: keyof UartEvents;
* removeListener: keyof UartEvents;
* receive: Uint8Array;
* receiveText: string;
*/
import ServiceHelper from "./ServiceHelper";

/**
* Events raised by the UART service
Expand Down
61 changes: 35 additions & 26 deletions extensions/src/doodlebot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,30 @@ const details: ExtensionMenuDisplayDetails = {
insetIconURL: "Replace with the name of your inset icon image file (which should be placed in the same directory as this file)"
};

export default class DoodlebotBlocks extends extension(details, "indicators") {

private doodlebot: Doodlebot;
export default class DoodlebotBlocks extends extension(details, "ui", "indicators") {
doodlebot: Doodlebot;
private indicator: Promise<{ close(): void; }>

init(env: Environment) {
this.openUI("Connect");
this.setIndicator("disconnected");
}

async connectToBLE() {
console.log("Getting BLE device");
const { bluetooth } = window.navigator;
if (!bluetooth) return alert("Your browser does not allow / support bluetooth.");
try {
const namePrefix = "Bluefruit52"; /* "Saira" "BBC micro:bit"; TODO: might delete? */
this.doodlebot = await Doodlebot.tryCreate("", "", bluetooth, { namePrefix });
await this.indicateFor({ position: "category", msg: "Connected to robot" }, 1000);
} catch (err) { DoodlebotBlocks.ProcessConnectionError(err); }
setDoodlebot(doodlebot: Doodlebot) {
this.doodlebot = doodlebot;
this.setIndicator("connected");
}

@buttonBlock("Test Robot")
test() {

async setIndicator(status: "connected" | "disconnected") {
if (this.indicator) (await this.indicator).close;
this.indicator = status == "connected"
? this.indicate({ position: "category", msg: "Connected to robot", type: "success" })
: this.indicate({ position: "category", msg: "Not connected to robot", type: "warning" });
}

@buttonBlock("Connect Robot")
connect() {
this.connectToBLE();
this.openUI("Connect");
}

@block({
Expand Down Expand Up @@ -66,17 +64,28 @@ export default class DoodlebotBlocks extends extension(details, "indicators") {
type: "command",
text: "clear display"
})
clearDisplay() {

async clearDisplay() {
await this.doodlebot?.display("clear");
}

private static ProcessConnectionError(err: Error) {
console.error(err);
if (err.message == "Bluetooth adapter not available.")
alert("Your device does not support BLE connections.");
if (err.message == "User cancelled the requestDevice() chooser.")
alert("You must select a device to connect to. Please try again.");
else if (err.message !== "User cancelled the requestDevice() chooser.")
alert("There was a problem connecting your device, please try again or request assistance.");

@block({
type: "command",
text: (direction, steps) => `drive ${direction} for ${steps} steps`,
args: [
{ type: "string", options: ["forward", "backward", "left", "right"], defaultValue: "forward" },
{ type: "number", defaultValue: 50 }
]
})
async drive(direction: "left" | "right" | "forward" | "backward", steps: number) {
const leftSteps = direction == "left" || direction == "backward" ? -steps : steps;
const rightSteps = direction == "right" || direction == "backward" ? -steps : steps;
const stepsPerSecond = 100;

await this.doodlebot?.motorCommand(
"steps",
{ steps: leftSteps, stepsPerSecond },
{ steps: rightSteps, stepsPerSecond }
);
}
}
33 changes: 24 additions & 9 deletions packages/scratch-gui/src/svelte/Modal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,30 @@
type ExtensionManager = _ExtensionManager & {
getAuxiliaryObject: (
id: ExtensionID,
name: ComponentName
name: ComponentName,
) => UIConstructor;
getExtensionInstance: (id: ExtensionID) => ExtensionInstance;
};
type VirtualMachine = _VirtualMachine & {
extensionManager: ExtensionManager;
};
async function untilDefined<T>(
getter: () => T,
delay: number = 100,
): Promise<T> {
let timeout: NodeJS.Timeout;
let value = getter();
while (!value) {
await new Promise((resolve) => {
clearTimeout(timeout);
timeout = setTimeout(resolve, delay);
});
value = getter();
}
clearTimeout(timeout);
return value;
}
</script>

<script lang="ts">
Expand All @@ -43,15 +60,13 @@
let constructed: any;
onMount(async () => {
const props = {
close,
extension: vm.extensionManager.getExtensionInstance(id),
};
const options = { target, props };
const constructor = vm.extensionManager.getAuxiliaryObject(
id,
component
const { extensionManager } = vm;
const extension = await untilDefined(() =>
extensionManager.getExtensionInstance(id),
);
const props = { close, extension };
const options = { target, props };
const constructor = extensionManager.getAuxiliaryObject(id, component);
constructed = new constructor(options);
return;
});
Expand Down

0 comments on commit 48c9700

Please sign in to comment.