Skip to content

Commit

Permalink
Handle bluetooth connecting, reconnecting, success, and failures (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
microbit-grace authored Jul 29, 2024
1 parent 802f5ea commit 5943a10
Show file tree
Hide file tree
Showing 6 changed files with 52 additions and 9 deletions.
19 changes: 19 additions & 0 deletions lib/bluetooth-device-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ class ServiceInfo<T extends Service> {
}
}

interface ConnectCallbacks {
onConnecting: () => void;
onReconnecting: () => void;
onFail: () => void;
onSuccess: () => void;
}

export class BluetoothDeviceWrapper {
// Used to avoid automatic reconnection during user triggered connect/disconnect
// or reconnection itself.
Expand Down Expand Up @@ -136,6 +143,7 @@ export class BluetoothDeviceWrapper {
private dispatchTypedEvent: TypedServiceEventDispatcher,
// We recreate this for the same connection and need to re-setup notifications for the old events
private events: Record<keyof ServiceConnectionEventMap, boolean>,
private callbacks: ConnectCallbacks,
) {
device.addEventListener(
"gattserverdisconnected",
Expand All @@ -158,6 +166,11 @@ export class BluetoothDeviceWrapper {
await this.gattConnectPromise;
return;
}
if (this.isReconnect) {
this.callbacks.onReconnecting();
} else {
this.callbacks.onConnecting();
}
this.duringExplicitConnectDisconnect++;
if (this.device.gatt === undefined) {
throw new Error(
Expand Down Expand Up @@ -233,16 +246,20 @@ export class BluetoothDeviceWrapper {
type: this.isReconnect ? "Reconnect" : "Connect",
message: "Bluetooth connect success",
});
this.callbacks.onSuccess();
} catch (e) {
this.logging.error("Bluetooth connect error", e);
this.logging.event({
type: this.isReconnect ? "Reconnect" : "Connect",
message: "Bluetooth connect failed",
});
await this.disconnectInternal(false);
this.callbacks.onFail();
throw new Error("Failed to establish a connection!");
} finally {
this.duringExplicitConnectDisconnect--;
// Reset isReconnect for next time
this.isReconnect = false;
}
}

Expand Down Expand Up @@ -433,6 +450,7 @@ export const createBluetoothDeviceWrapper = async (
logging: Logging,
dispatchTypedEvent: TypedServiceEventDispatcher,
addedServiceListeners: Record<keyof ServiceConnectionEventMap, boolean>,
callbacks: ConnectCallbacks,
): Promise<BluetoothDeviceWrapper | undefined> => {
try {
// Reuse our connection objects for the same device as they
Expand All @@ -444,6 +462,7 @@ export const createBluetoothDeviceWrapper = async (
logging,
dispatchTypedEvent,
addedServiceListeners,
callbacks,
);
deviceIdToWrapper.set(device.id, bluetooth);
await bluetooth.connect();
Expand Down
13 changes: 12 additions & 1 deletion lib/bluetooth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export class MicrobitWebBluetoothConnection
});
} finally {
this.connection = undefined;
this.setStatus(ConnectionStatus.NOT_CONNECTED);
this.setStatus(ConnectionStatus.DISCONNECTED);
this.logging.log("Disconnection complete");
this.logging.event({
type: "Bluetooth-info",
Expand Down Expand Up @@ -154,14 +154,25 @@ export class MicrobitWebBluetoothConnection
if (!this.connection) {
const device = await this.chooseDevice();
if (!device) {
this.setStatus(ConnectionStatus.NO_AUTHORIZED_DEVICE);
return;
}
this.connection = await createBluetoothDeviceWrapper(
device,
this.logging,
this.dispatchTypedEvent.bind(this),
this.activeEvents,
{
onConnecting: () => this.setStatus(ConnectionStatus.CONNECTING),
onReconnecting: () => this.setStatus(ConnectionStatus.RECONNECTING),
onSuccess: () => this.setStatus(ConnectionStatus.CONNECTED),
onFail: () => {
this.setStatus(ConnectionStatus.DISCONNECTED);
this.connection = undefined;
},
},
);
return;
}
// TODO: timeout unification?
// Connection happens inside createBluetoothDeviceWrapper.
Expand Down
15 changes: 14 additions & 1 deletion lib/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,27 @@ export enum ConnectionStatus {
* but has not been connected via the browser security UI.
*/
NO_AUTHORIZED_DEVICE = "NO_AUTHORIZED_DEVICE",
/**
* Disconnecting.
*/
DISCONNECTING = "DISCONNECTING",
/**
* Authorized device available but we haven't connected to it.
*/
NOT_CONNECTED = "NOT_CONNECTED",
DISCONNECTED = "DISCONNECTED",
/**
* Connected.
*/
CONNECTED = "CONNECTED",
/**
* Connecting.
*/
CONNECTING = "CONNECTING",
/**
* Reconnecting. When there is unexpected disruption in the connection,
* a reconnection is attempted.
*/
RECONNECTING = "RECONNECTING",
}

export class FlashDataError extends Error {}
Expand Down
8 changes: 4 additions & 4 deletions lib/usb-radio-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ export class MicrobitRadioBridgeConnection
this.setStatus(e.status);
this.serialSession?.dispose();
} else {
this.status = ConnectionStatus.NOT_CONNECTED;
this.status = ConnectionStatus.DISCONNECTED;
if (
currentStatus === ConnectionStatus.NOT_CONNECTED &&
currentStatus === ConnectionStatus.DISCONNECTED &&
this.serialSessionOpen
) {
this.serialSession?.connect();
Expand Down Expand Up @@ -181,7 +181,7 @@ export class MicrobitRadioBridgeConnection

private statusFromDelegate(): ConnectionStatus {
return this.delegate.status == ConnectionStatus.CONNECTED
? ConnectionStatus.NOT_CONNECTED
? ConnectionStatus.DISCONNECTED
: this.delegate.status;
}
}
Expand Down Expand Up @@ -336,7 +336,7 @@ class RadioBridgeSerialSession {
this.delegate.removeEventListener("serialerror", this.serialErrorListener);
await this.delegate.softwareReset();

this.onStatusChanged(ConnectionStatus.NOT_CONNECTED);
this.onStatusChanged(ConnectionStatus.DISCONNECTED);
}

private async sendCmdWaitResponse(
Expand Down
4 changes: 2 additions & 2 deletions lib/usb.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,10 @@ describeDeviceOnly("MicrobitWebUSBConnection (WebUSB supported)", () => {
await connection.disconnect();
connection.dispose();

expect(connection.status).toEqual(ConnectionStatus.NOT_CONNECTED);
expect(connection.status).toEqual(ConnectionStatus.DISCONNECTED);
expect(events).toEqual([
ConnectionStatus.CONNECTED,
ConnectionStatus.NOT_CONNECTED,
ConnectionStatus.DISCONNECTED,
]);
});
});
2 changes: 1 addition & 1 deletion lib/usb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ export class MicrobitWebUSBConnection
});
} finally {
this.connection = undefined;
this.setStatus(ConnectionStatus.NOT_CONNECTED);
this.setStatus(ConnectionStatus.DISCONNECTED);
this.logging.log("Disconnection complete");
this.logging.event({
type: "WebUSB-info",
Expand Down

0 comments on commit 5943a10

Please sign in to comment.