usb: show error message if opening device fails

So far, we've just seen this on Linux when the user doesn't have the
correct udev rules installed. We'll have to wait for more user feedback
to see what other situations might cause this.
This commit is contained in:
David Lechner
2026-01-25 16:53:39 -06:00
parent b6cf5e8491
commit 209b7ef974
5 changed files with 70 additions and 1 deletions
+19
View File
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2025-2026 The Pybricks Authors
import { Toast } from '@blueprintjs/core';
import { act } from '@testing-library/react';
import React from 'react';
import { testRender } from '../../../test';
import { accessDenied } from './AccessDenied';
it('should dismiss when close is clicked', async () => {
const callback = jest.fn();
const toast = accessDenied(callback, undefined as never);
const [user, message] = testRender(<Toast {...toast} />);
await act(() => user.click(message.getByRole('button', { name: /close/i })));
expect(callback).toHaveBeenCalledWith('dismiss');
});
+26
View File
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2025-2026 The Pybricks Authors
import { Intent } from '@blueprintjs/core';
import { Error } from '@blueprintjs/icons';
import React from 'react';
import type { CreateToast } from '../../toasterTypes';
import { isLinux } from '../../utils/os';
import { useI18n } from './i18n';
const AccessDenied: React.FunctionComponent = () => {
const i18n = useI18n();
return (
<>
<p>{i18n.translate('accessDenied.message')}</p>
{isLinux() && <p>{i18n.translate('accessDenied.linuxSuggestion')}</p>}
</>
);
};
export const accessDenied: CreateToast = (onAction) => ({
message: <AccessDenied />,
icon: <Error />,
intent: Intent.DANGER,
onDismiss: () => onAction('dismiss'),
});
+2
View File
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2025-2026 The Pybricks Authors
import { accessDenied } from './AccessDenied';
import { alreadyInUse } from './AlreadyInUse';
import { newPybricksProfile } from './NewPybricksProfile';
import { noWebUsb } from './NoWebUsb';
@@ -8,6 +9,7 @@ import { oldFirmware } from './OldFirmware';
// gathers all of the alert creation functions for passing up to the top level
export default {
accessDenied,
alreadyInUse,
newPybricksProfile,
noWebUsb,
+4
View File
@@ -4,6 +4,10 @@
"suggestion": "Use a supported browser such as Google Chrome or Microsoft Edge.",
"action": "More Info"
},
"accessDenied": {
"message": "Access to the USB device was denied.",
"linuxSuggestion": "On Linux, ensure that you have the correct udev rules installed."
},
"alreadyInUse": {
"message": "This hub is already in use by another application."
},
+19 -1
View File
@@ -154,7 +154,25 @@ function* handleUsbConnectPybricks(hotPlugDevice?: USBDevice): Generator {
continue;
}
// TODO: show error message to user here
// Only show error to the user if they initiated the connection.
if (hotPlugDevice === undefined) {
if (openErr.name === 'SecurityError') {
// Known causes:
// - Linux without proper udev rules to allow access to USB devices
// - Trying to access a device on a host machine when the USB
// device is shared with a VM guest OS.
// Other suspected causes:
// - Issues with permissions in containerized apps (e.g. Snaps on Ubuntu)
yield* put(alertsShowAlert('usb', 'accessDenied'));
} else {
yield* put(
alertsShowAlert('alerts', 'unexpectedError', {
error: openErr,
}),
);
}
}
console.error('Failed to open USB device:', openErr);
yield* put(usbDidFailToConnectPybricks());
yield* cleanup();