From d57d1190d63ad3e9cda3cd2930c8567f9d454521 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 24 Jan 2026 22:38:21 +0000 Subject: [PATCH 1/3] usb/sagas: Retry USB device open on Linux if SecurityError occurs On slow machines, udev rules may not have finished processing by the time we try to open the USB device. This causes a SecurityError, so we add a retry loop to handle this case. Closes: https://github.com/pybricks/support/issues/2372 --- src/usb/sagas.ts | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/src/usb/sagas.ts b/src/usb/sagas.ts index 80323e28..570df488 100644 --- a/src/usb/sagas.ts +++ b/src/usb/sagas.ts @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2025 The Pybricks Authors +// Copyright (c) 2025-2026 The Pybricks Authors import { firmwareVersion } from '@pybricks/firmware'; import { AnyAction } from 'redux'; @@ -128,6 +128,7 @@ function* handleUsbConnectPybricks(hotPlugDevice?: USBDevice): Generator { return; } + // TODO: show unexpected error message to user here console.error('Failed to request USB device:', reqDeviceErr); yield* put(usbDidFailToConnectPybricks()); yield* cleanup(); @@ -140,13 +141,27 @@ function* handleUsbConnectPybricks(hotPlugDevice?: USBDevice): Generator { usbDevice = hotPlugDevice; } - const [, openErr] = yield* call(() => maybe(usbDevice.open())); - if (openErr) { - // TODO: show error message to user here - console.error('Failed to open USB device:', openErr); - yield* put(usbDidFailToConnectPybricks()); - yield* cleanup(); - return; + for (let retry = 1; ; retry++) { + const [, openErr] = yield* call(() => maybe(usbDevice.open())); + if (openErr) { + // On Linux/Android, the udev rules could still be processing, try + // a few times before giving up. + if (openErr.name === 'SecurityError' && retry <= 5) { + console.debug( + `Retrying USB device open (${retry}/5) after SecurityError on Linux`, + ); + yield* delay(100); + continue; + } + + // TODO: show error message to user here + console.error('Failed to open USB device:', openErr); + yield* put(usbDidFailToConnectPybricks()); + yield* cleanup(); + return; + } + + break; } exitStack.push(() => usbDevice.close().catch(console.debug)); From 532852d5912afeb522c93a5d74c1de133ca48a1f Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 24 Jan 2026 23:06:32 +0000 Subject: [PATCH 2/3] github: upload build artifact Add a step to upload the build artifact after building the project. This will make it easy for people to test it without having to build it themselves. --- .github/workflows/test-pull-request.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index 236c536d..b4be39b7 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -29,4 +29,13 @@ jobs: - uses: codecov/codecov-action@v3 with: directory: coverage + - name: Set env to labs + run: | + echo "REACT_APP_NAME=Pybricks Labs" >> $GITHUB_ENV + echo "REACT_APP_SUFFIX=-labs" >> $GITHUB_ENV + echo "REACT_APP_VERSION=$GITHUB_SHA" >> $GITHUB_ENV - run: yarn build + - uses: actions/upload-artifact@v6 + with: + name: pybricks-labs + path: build From 7b8a8094e4957f6aebb665f48b0e5212bd6722df Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 25 Jan 2026 20:23:00 +0000 Subject: [PATCH 3/3] usb/sagas: Fix command response race condition Fix a race condition where a response to a USB command could be missed if it arrives before the USB transferOut() call returns. This is done by using a channel to buffer incoming responses. Fixes: https://github.com/pybricks/support/issues/2467 --- src/usb/sagas.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/usb/sagas.ts b/src/usb/sagas.ts index 570df488..eb9a1c96 100644 --- a/src/usb/sagas.ts +++ b/src/usb/sagas.ts @@ -454,6 +454,12 @@ function* handleUsbConnectPybricks(hotPlugDevice?: USBDevice): Generator { writeCommand.matches(a), ); + // Response may come before request returns, so we need to buffer them + // in a channel to avoid missing responses. + const responseChannel = yield* actionChannel( + usbDidReceivePybricksMessageResponse, + ); + for (;;) { const action = yield* take(chan); @@ -525,7 +531,7 @@ function* handleUsbConnectPybricks(hotPlugDevice?: USBDevice): Generator { } const { response, timeout } = yield* race({ - response: take(usbDidReceivePybricksMessageResponse), + response: take(responseChannel), timeout: delay(1000), });