diff --git a/src/ble/alerts/FailedToConnect.tsx b/src/ble/alerts/FailedToConnect.tsx
new file mode 100644
index 000000000..5e56aa0a0
--- /dev/null
+++ b/src/ble/alerts/FailedToConnect.tsx
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2023 The Pybricks Authors
+
+import { Intent } from '@blueprintjs/core';
+import React from 'react';
+import type { CreateToast } from '../../toasterTypes';
+import { useI18n } from './i18n';
+
+const FailedToConnect: React.VoidFunctionComponent = () => {
+ const i18n = useI18n();
+ return (
+ <>
+
{i18n.translate('failedToConnect.message')}
+ {i18n.translate('failedToConnect.suggestion')}
+ >
+ );
+};
+
+export const failedToConnect: CreateToast = (onAction) => ({
+ message: ,
+ icon: 'error',
+ intent: Intent.DANGER,
+ onDismiss: () => onAction('dismiss'),
+});
diff --git a/src/ble/alerts/index.ts b/src/ble/alerts/index.ts
index 22b7ffc04..7bd17973d 100644
--- a/src/ble/alerts/index.ts
+++ b/src/ble/alerts/index.ts
@@ -1,7 +1,8 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2022 The Pybricks Authors
+// Copyright (c) 2022-2023 The Pybricks Authors
import { bluetoothNotAvailable } from './BluetoothNotAvailable';
+import { failedToConnect } from './FailedToConnect';
import { missingService } from './MissingService';
import { noGatt } from './NoGatt';
import { noHub } from './NoHub';
@@ -11,6 +12,7 @@ import { oldFirmware } from './OldFirmware';
// gathers all of the alert creation functions for passing up to the top level
export default {
bluetoothNotAvailable,
+ failedToConnect,
missingService,
noGatt,
noHub,
diff --git a/src/ble/alerts/translations/en.json b/src/ble/alerts/translations/en.json
index 97c4da8bc..7efe8d07a 100644
--- a/src/ble/alerts/translations/en.json
+++ b/src/ble/alerts/translations/en.json
@@ -13,6 +13,10 @@
"noGatt": {
"message": "The web browser did not give permission to use Bluetooth Low Energy."
},
+ "failedToConnect": {
+ "message": "Failed to connect.",
+ "suggestion": "Is the hub still turned on with no programs running?"
+ },
"missingService": {
"message": "Connected to hub but failed to get {serviceName} service.",
"suggestion1": "Ensure that you are using the most recent firmware.",
diff --git a/src/ble/sagas.ts b/src/ble/sagas.ts
index e5aa87e82..e6d881f7a 100644
--- a/src/ble/sagas.ts
+++ b/src/ble/sagas.ts
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
-// Copyright (c) 2020-2022 The Pybricks Authors
+// Copyright (c) 2020-2023 The Pybricks Authors
//
// Manages connection to a Bluetooth Low Energy device running Pybricks firmware.
@@ -146,6 +146,9 @@ function* handleBleConnectPybricks(): Generator {
}),
);
+ // TODO: remove debug print
+ console.log('device', device);
+
if (!device) {
yield* put(alertsShowAlert('ble', 'noHub'));
yield* put(bleDidFailToConnectPybricks());
@@ -181,7 +184,39 @@ function* handleBleConnectPybricks(): Generator {
defer.push(() => disconnectChannel.close());
- const server = yield* call(() => gatt.connect());
+ const timeout = setTimeout(() => {
+ // TODO: remove debug print
+ console.log('calling disconnect');
+ gatt.disconnect();
+ }, 10000);
+
+ const server = yield* call(() =>
+ gatt.connect().catch((err) => {
+ // TODO: remove debug print
+ console.error(err);
+
+ // timeout or other connection problem
+ if (err instanceof DOMException && err.name === 'AbortError') {
+ return undefined;
+ }
+
+ throw err;
+ }),
+ );
+
+ clearTimeout(timeout);
+
+ // TODO: remove debug print
+ console.log(server);
+
+ if (!server) {
+ yield* put(alertsShowAlert('ble', 'failedToConnect'));
+ yield* put(bleDidFailToConnectPybricks());
+ return;
+ }
+
+ // TODO: remove debug print
+ console.log('server', server);
defer.push(() => server.disconnect());