Skip to content

Commit

Permalink
Add no-name micro:bit connection
Browse files Browse the repository at this point in the history
  • Loading branch information
r59q committed May 15, 2024
1 parent 64101a0 commit 5a7683e
Show file tree
Hide file tree
Showing 10 changed files with 246 additions and 27 deletions.
4 changes: 2 additions & 2 deletions src/__tests__/microbit-bluetooth-connection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,16 +170,16 @@ describe('Microbit Bluetooth interface tests', () => {

test('Request device yields device', async () => {
const device = await MicrobitBluetooth.requestDevice(
'vatav',
TypingUtils.emptyFunction,
'vatav',
);
expect(device).toBeDefined();
});

test('Can connect to requested device', async () => {
const device = await MicrobitBluetooth.requestDevice(
'vatav',
TypingUtils.emptyFunction,
'vatav',
);

const con = await MicrobitBluetooth.createMicrobitBluetooth(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,23 +38,34 @@
let timeouted = writable<boolean>(false);
const connectButtonClicked = () => {
const connectUsingPatternName = () => {
if (!isInputPatternValid()) {
attemptedToPairWithInvalidPattern = true;
return;
}
const name = MBSpecs.Utility.patternToName($patternMatrixState);
attemptToConnect(name);
};
const attemptToConnect = (name?: string) => {
timeoutProgress.set(0);
if (isConnecting) {
// Safeguard to prevent trying to connect multiple times at once
return;
}
isConnecting = true;
let name = MBSpecs.Utility.patternToName($patternMatrixState);
const connectionResult = () => {
if (deviceState == DeviceRequestStates.INPUT) {
return Microbits.assignInput(name);
if (name) {
return Microbits.assignInput(name);
}
return Microbits.assignInputNoName();
} else {
return Microbits.assignOutput(name);
if (name) {
return Microbits.assignOutput(name);
}
return Microbits.assignOutputNoName();
}
};
Expand Down Expand Up @@ -83,7 +94,7 @@
if (event.code !== 'Enter') {
return;
}
void connectButtonClicked();
void connectUsingPatternName();
}
function updateMatrix(matrix: boolean[]): void {
Expand All @@ -102,6 +113,10 @@
// Resets the bluetooth connection prompt for cancelled device requests
$state.requestDeviceWasCancelled = false;
});
const handleSearchWithoutName = () => {
attemptToConnect();
};
</script>

<main>
Expand All @@ -111,6 +126,14 @@

{#if $state.requestDeviceWasCancelled && !isConnecting}
<p class="text-warning mb-1">{$t('popup.connectMB.bluetooth.cancelledConnection')}</p>
<p class="text-warning mb-1">
Couldn't find your microbit on the list?
<span
class="underline text-link cursor-pointer select-none"
on:click={handleSearchWithoutName}>
Search for all nearby micro:bits
</span>
</p>
{/if}
{#if attemptedToPairWithInvalidPattern}
<p class="text-warning mb-1">{$t('popup.connectMB.bluetooth.invalidPattern')}</p>
Expand Down Expand Up @@ -138,7 +161,7 @@
<PatternMatrix matrix={$patternMatrixState} onMatrixChange={updateMatrix} />
</div>
</div>
<StandardButton onClick={connectButtonClicked}
<StandardButton onClick={connectUsingPatternName}
>{$t('popup.connectMB.bluetooth.connect')}</StandardButton>
{/if}
<!-- </div> -->
Expand Down
5 changes: 4 additions & 1 deletion src/pages/training/TrainingPage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,10 @@
<div class="w-full pt-5 text-white pb-5">
<TrainModelButton
selectedOption={selectedModelOption}
onClick={() => {resetLoss(); trackModelEvent();}}
onClick={() => {
resetLoss();
trackModelEvent();
}}
onTrainingIteration={trainingIterationHandler} />
</div>
{/if}
Expand Down
7 changes: 5 additions & 2 deletions src/script/microbit-interfacing/MicrobitBluetooth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,14 +340,17 @@ export class MicrobitBluetooth {
* Fired if the request failed.
*/
public static async requestDevice(
name: string,
onRequestFailed: (e: Error) => void,
name?: string,
): Promise<BluetoothDevice> {
return new Promise<BluetoothDevice>((resolve, reject) => {
const filters = name
? [{ namePrefix: `BBC micro:bit [${name}]` }]
: [{ namePrefix: `BBC micro:bit` }];
try {
navigator.bluetooth
.requestDevice({
filters: [{ namePrefix: `BBC micro:bit [${name}]` }],
filters: filters,
optionalServices: [
MBSpecs.Services.UART_SERVICE,
MBSpecs.Services.ACCEL_SERVICE,
Expand Down
195 changes: 192 additions & 3 deletions src/script/microbit-interfacing/Microbits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import ConnectionBehaviours from './connection-behaviours/ConnectionBehaviours';
import { get, writable } from 'svelte/store';
import MBSpecs from './MBSpecs';
import MicrobitBluetooth from './MicrobitBluetooth';
import { onCatastrophicError, outputting } from '../stores/uiStore';
import { outputting } from '../stores/uiStore';
import MicrobitUSB from './MicrobitUSB';
import type ConnectionBehaviour from './connection-behaviours/ConnectionBehaviour';
import TypingUtils from '../TypingUtils';
Expand Down Expand Up @@ -171,6 +171,117 @@ class Microbits {
return this.assignedOutputMicrobit;
}

/**
* Attempts to assign and connect via bluetooth.
* @param name The expected name of the microbit.
* @return Returns true if the connection was successful, else false.
*/
public static async assignInputNoName(): Promise<boolean> {
// This function is long, and ought to be split up to make it easier to understand, but this is the short explanation
// The goal is to save a MicrobitBluetooth instance to the field this.assignedInputMicrobit.
// To do this we create a bluetooth connection, `MicrobitBluetooth.createMicrobitBluetooth`
// This function needs a lot of callbacks to handle behaviours for connection, reconnection, disconnection, etc.
// These callbacks are what makes this function so long, as they are dependent on the state of the application
const connectionBehaviour = ConnectionBehaviours.getInputBehaviour();

const onInitialInputConnect = (microbit: MicrobitBluetooth) => {
this.assignedInputMicrobit = microbit;

connectionBehaviour.onConnected();
Microbits.listenToInputServices()
.then(() => {
connectionBehaviour.onReady();
})
.catch(reason => {
console.log(reason);
});
};

const onInputDisconnect = (manual?: boolean) => {
this.inputBuildVersion = undefined;
if (this.isInputOutputTheSame()) {
ConnectionBehaviours.getOutputBehaviour().onDisconnected();
}
if (manual) {
if (this.isInputAssigned()) {
ConnectionBehaviours.getInputBehaviour().onExpelled(manual, true);
ConnectionBehaviours.getOutputBehaviour().onExpelled(manual, true);
this.clearAssignedOutputReference();
this.clearAssignedInputReference();
}
} else {
connectionBehaviour.onDisconnected();
// this.isInputReconnecting = true; // We dont offer reconnect, because dont have a name
}
this.clearBluetoothServiceActionQueue();
};

const onInputReconnect = (microbit: MicrobitBluetooth) => {
this.isInputReconnecting = false;
if (this.inputFlaggedForDisconnect) {
// User has disconnected during the reconnect process,
// and the connection was reestablished, disconnect safely
void this.disconnectInputSafely(microbit);
this.inputFlaggedForDisconnect = false;
return;
}
this.assignedInputMicrobit = microbit;
if (this.isInputOutputTheSame()) {
ConnectionBehaviours.getOutputBehaviour().onConnected();
}
connectionBehaviour.onConnected();
Microbits.listenToInputServices()
.then(() => {
clearTimeout(this.inputVersionIdentificationTimeout);
if (this.isInputOutputTheSame()) {
this.assignedOutputMicrobit = microbit;
Microbits.listenToOutputServices()
.then(() => {
clearTimeout(this.outputVersionIdentificationTimeout);
connectionBehaviour.onReady();
ConnectionBehaviours.getOutputBehaviour().onReady();
})
.catch(reason => {
console.error(reason);
});
} else {
connectionBehaviour.onReady();
}
})
.catch(reason => {
console.error(reason);
});
};

const onInputReconnectFailed = () => {
ConnectionBehaviours.getInputBehaviour().onExpelled(false, true);
ConnectionBehaviours.getOutputBehaviour().onExpelled(false, true);
this.clearAssignedOutputReference();
};

try {
const request = await MicrobitBluetooth.requestDevice(
this.onFailedConnection(connectionBehaviour),
);
await MicrobitBluetooth.createMicrobitBluetooth(
request,
onInitialInputConnect,
onInputDisconnect,
this.onFailedConnection(connectionBehaviour),
onInputReconnect,
onInputReconnectFailed,
);

connectionBehaviour.onAssigned(this.getInput());
this.inputVersion = this.getInput().getVersion();
return true;
} catch (e) {
console.error(e);
this.onFailedConnection(connectionBehaviour)(e as Error);
}
return false;
}

/**
* Attempts to assign and connect via bluetooth.
* @param name The expected name of the microbit.
Expand Down Expand Up @@ -267,8 +378,8 @@ class Microbits {

try {
const request = await MicrobitBluetooth.requestDevice(
name,
this.onFailedConnection(connectionBehaviour),
name,
);
await MicrobitBluetooth.createMicrobitBluetooth(
request,
Expand Down Expand Up @@ -351,6 +462,84 @@ class Microbits {
}
}

/**
* Attempts to assign and connect via bluetooth.
* @param name The expected name of the microbit.
* @return Returns true if the connection was successful, else false.
*/
public static async assignOutputNoName(): Promise<boolean> {
const connectionBehaviour: ConnectionBehaviour =
ConnectionBehaviours.getOutputBehaviour();

const onInitialOutputConnect = (microbit: MicrobitBluetooth) => {
this.assignedOutputMicrobit = microbit;
connectionBehaviour.onConnected();
this.listenToOutputServices()
.then(() => {
connectionBehaviour.onReady();
})
.catch(e => {
console.log(e);
});
};

const onOutputDisconnect = (manual?: boolean) => {
this.outputBuildVersion = undefined;
if (manual) {
if (this.isOutputAssigned()) {
ConnectionBehaviours.getOutputBehaviour().onExpelled(manual);
this.clearAssignedOutputReference();
}
} else {
// this.isOutputReconnecting = true; // We dont offer reconnection for no-named connections
ConnectionBehaviours.getOutputBehaviour().onDisconnected();
}
this.clearBluetoothServiceActionQueue();
};

const onOutputReconnect = (microbit: MicrobitBluetooth) => {
this.isOutputReconnecting = false;
if (this.outputFlaggedForDisconnect) {
this.outputFlaggedForDisconnect = false;
void this.disconnectOutputSafely(microbit);
return;
}
this.assignedOutputMicrobit = microbit;
connectionBehaviour.onConnected();
this.listenToOutputServices()
.then(() => {
connectionBehaviour.onReady();
})
.catch(e => {
console.log(e);
});
};

const onOutputReconnectFailed = () => {
connectionBehaviour.onExpelled(false, false);
};

try {
const bluetoothDevice = await MicrobitBluetooth.requestDevice(
this.onFailedConnection(connectionBehaviour),
);
await MicrobitBluetooth.createMicrobitBluetooth(
bluetoothDevice,
onInitialOutputConnect,
onOutputDisconnect,
this.onFailedConnection(connectionBehaviour),
onOutputReconnect,
onOutputReconnectFailed,
);
connectionBehaviour.onAssigned(this.getOutput());
this.outputVersion = this.getOutput().getVersion();
return true;
} catch (e) {
this.onFailedConnection(connectionBehaviour)(e as Error);
}
return false;
}

/**
* Attempts to assign and connect via bluetooth.
* @param name The expected name of the microbit.
Expand Down Expand Up @@ -412,8 +601,8 @@ class Microbits {

try {
const bluetoothDevice = await MicrobitBluetooth.requestDevice(
name,
this.onFailedConnection(connectionBehaviour),
name,
);
await MicrobitBluetooth.createMicrobitBluetooth(
bluetoothDevice,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ interface ConnectionBehaviour {
* @param {string} name
* The name of the micro:bit.
*/
onAssigned(microbit: MicrobitBluetooth, name: string): void;
onAssigned(microbit: MicrobitBluetooth, name?: string): void;

/**
* What should happen when the micro:bit gets connected via Bluetooth
* @param name Name of the micro:bit
*/
onConnected(name: string): void;
onConnected(name?: string): void;

/**
* What should happen when the microbit is ready?
Expand Down
Loading

0 comments on commit 5a7683e

Please sign in to comment.