Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"postStartCommand": "pnpm config set store-dir /home/vscode/.local/share/pnpm/store && pnpm install",
"customizations": {
"vscode": {
"extensions": [
"ms-vscode.vscode-typescript-next",
"esbenp.prettier-vscode",
"svelte.svelte-vscode"
],
"settings": {
"[svelte]": {
"editor.defaultFormatter": "svelte.svelte-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.formatOnSave": true,
"editor.tabSize": 2,
"svelte.enable-ts-plugin": true
}
}
},
"features": {
"ghcr.io/devcontainers/features/node:latest": {
"version": "20"
},
"ghcr.io/pmalacho-mit/devcontainer-features/git-subrepo:latest": {}
},
"image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04"
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
**/.pnpm-store
.DS_Store
./**/.DS_Store

Expand Down
241 changes: 151 additions & 90 deletions extensions/src/doodlebot/Connect.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
<script lang="ts">
import type Extension from ".";
import { ReactiveInvoke, reactiveInvoke, color } from "$common";
import Doodlebot from "./Doodlebot";
import {
type BLEDeviceWithUartService,
getBLEDeviceWithUartService,
} from "./ble";
import { onDestroy } from "svelte";

export let extension: Extension;

Expand All @@ -12,62 +16,63 @@
const invoke: ReactiveInvoke<Extension> = (functionName, ...args) =>
reactiveInvoke((extension = extension), functionName, args);

const storageKeys = {
ssid: "doodlebot-ssid",
password: "doodlebot-password",
ip: "doodlebot-ip",
};

const ipPrefix = "192.168.0.";
let connected = extension.connected;

let error: string;
let ssid = localStorage.getItem(storageKeys.ssid) ?? "";
let password = localStorage.getItem(storageKeys.password) ?? "";
let error: string | null = null;

const savedIP = localStorage.getItem(storageKeys.ip);
const ipParts = [
savedIP?.split(".")[0] ?? "192",
savedIP?.split(".")[1] ?? "168",
savedIP?.split(".")[2] ?? "0",
savedIP?.split(".")[3] ?? "0",
];
let bleDevice: BLEDeviceWithUartService | null = null;
let topLevelDomain: string | null = null;
let topLevelDomainOverride: string =
new URLSearchParams(window.location.search).get("tld") ?? "";

const inputs = {
ssid: null as HTMLInputElement,
password: null as HTMLInputElement,
const errorOut = (message: string, err?: Error) => {
invoke("setIndicator", "disconnected");
if (err) console.error(err);
error = message;
};

const createConnection = async () => {
const ipOverride =
ipParts.filter(Boolean).length === 4 ? ipParts.join(".") : undefined;

const retrieveDevice = async () => {
try {
const doodlebot = await Doodlebot.tryCreate(bluetooth, {
credentials: { ssid, password, ipOverride },
requestBluetooth: extension.requestBluetooth.bind(extension),
saveIP: (ip) => localStorage.setItem(storageKeys.ip, ip),
});

invoke("setDoodlebot", doodlebot);
localStorage.setItem(storageKeys.ssid, ssid);
localStorage.setItem(storageKeys.password, password);
if (ipOverride) localStorage.setItem(storageKeys.ip, ipOverride);
close();
const result = await getBLEDeviceWithUartService(bluetooth);
if ("error" in result) {
invoke("setIndicator", "disconnected");
error = result.error;
} else {
bleDevice = result;
topLevelDomain = result.device.name;
if (!topLevelDomain.endsWith(".direct.mitlivinglab.org"))
topLevelDomain += ".direct.mitlivinglab.org";
}
} catch (err) {
invoke("setIndicator", "disconnected");
console.error(err);
error =
errorOut(
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;
: err.message,
err
);
}
};

let showAdvanced = true;
const setConnection = () => {
if (!bleDevice || !topLevelDomain)
return errorOut("You must select a device to connect to.");
extension.setDoodlebot(topLevelDomainOverride || topLevelDomain, bleDevice);
extension.connected = true;
close();
};

let showAdvanced = false;

onDestroy(() => {
if (!connected)
try {
extension.setIndicator("disconnected");
} catch (e) {}
});
</script>

<div
Expand All @@ -76,33 +81,39 @@
style:background-color={color.ui.white}
style:color={color.text.primary}
>
{#if error}
<div class="error">
{error}
{#if connected}
<h1>You're connected to doodlebot!</h1>
<div>
If you'd like to reconnect, or connect to a different device, you must
reload this page.
</div>
{/if}
{#if bluetooth}
<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="password"
placeholder="e.g. 12345"
/>
</p>
<button on:click={() => window.location.reload()}> Reload </button>
</div>
{:else}
{#if error}
<div class="error">
{error}
</div>
{/if}
{#if bluetooth}
<h1>Please connect to a doodlebot...</h1>
<div>
<h3>...by selecting a bluetooth device</h3>
<button class="open" on:click={retrieveDevice}>
Open Bluetooth Menu
</button>
{#if bleDevice}
You've selected 🤖 <strong>{bleDevice.device.name}</strong>.
{/if}
</div>
<div style:margin-top="20px">
<button
class="connect"
disabled={!bleDevice || !topLevelDomain}
on:click={setConnection}>Connect</button
>
</div>
<div>
<button
class="collapser"
Expand All @@ -115,31 +126,18 @@
style:max-height={showAdvanced ? "fit-content" : "0"}
>
<p>
IP:
{#each ipParts as part, i}
<input
class="ip"
bind:this={inputs.password}
bind:value={ipParts[i]}
type="text"
placeholder="e.g. 192"
/>
{i < ipParts.length - 1 ? "." : ""}
{/each}
Use top level domain:
<input type="text" bind:value={topLevelDomainOverride} />
{#if topLevelDomain}
(instead of '{topLevelDomain}')
{/if}
</p>
</div>
</div>
</div>
<div>
<h3>2. Select bluetooth device</h3>

<button disabled={!password || !ssid} on:click={createConnection}>
Open Bluetooth Menu
</button>
</div>
{:else}
Uh oh! Your browser does not support bluetooth. Here's how to fix that...
TBD
{:else}
Uh oh! Your browser does not support bluetooth. Please contact an
instructor.
{/if}
{/if}
</div>

Expand All @@ -165,7 +163,70 @@
outline: none;
}

.ip {
width: 3rem;
.open {
background-color: dodgerblue;
border: 1px solid dodgerblue;
border-radius: 4px;
box-shadow: rgba(0, 0, 0, 0.1) 0 2px 4px 0;
box-sizing: border-box;
color: #fff;
cursor: pointer;
font-family:
"Akzidenz Grotesk BQ Medium",
-apple-system,
BlinkMacSystemFont,
sans-serif;
font-size: 16px;
font-weight: 600;
outline: none;
outline: 0;
padding: 5px 15px;
text-align: center;
transform: translateY(0);
transition:
transform 150ms,
box-shadow 150ms;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
}

/* CSS */
.connect {
background-color: #13aa52;
border: 1px solid #13aa52;
border-radius: 4px;
box-shadow: rgba(0, 0, 0, 0.1) 0 2px 4px 0;
box-sizing: border-box;
color: #fff;
cursor: pointer;
font-family:
"Akzidenz Grotesk BQ Medium",
-apple-system,
BlinkMacSystemFont,
sans-serif;
font-size: 16px;
font-weight: 600;
outline: none;
outline: 0;
padding: 10px 25px;
text-align: center;
transform: translateY(0);
transition:
transform 150ms,
box-shadow 150ms;
user-select: none;
-webkit-user-select: none;
touch-action: manipulation;
}

.connect:disabled {
opacity: 0.5;
cursor: not-allowed;
}

.connect:not(:disabled):hover {
box-shadow: rgba(0, 0, 0, 0.15) 0 3px 9px 0;
transform: translateY(-2px);
}
</style>
Loading
Loading