diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index 236c536d5..b4be39b74 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 diff --git a/src/usb/sagas.ts b/src/usb/sagas.ts index 80323e28d..eb9a1c96f 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)); @@ -439,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); @@ -510,7 +531,7 @@ function* handleUsbConnectPybricks(hotPlugDevice?: USBDevice): Generator { } const { response, timeout } = yield* race({ - response: take(usbDidReceivePybricksMessageResponse), + response: take(responseChannel), timeout: delay(1000), });