Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/test-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +32 to +41
Copy link
Member

@laurensvalk laurensvalk Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this something we would occasionally add for particular PRs that need testing? (Otherwise how do we handle more than one PR?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't automatically publish anywhere. It it just uploads the artifact to GitHub. One could download it and run it locally with serve.py to test a PR without having to build it themselves. Or we can throw it on labs.pybricks.com if we want wider testing.

39 changes: 30 additions & 9 deletions src/usb/sagas.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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();
Expand All @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we make a issue/task for TODOs like these?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want one issue per error message or one for all of them? I don't remember if we have an issue for this in general or not.

console.error('Failed to open USB device:', openErr);
yield* put(usbDidFailToConnectPybricks());
yield* cleanup();
return;
}

break;
}

exitStack.push(() => usbDevice.close().catch(console.debug));
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -510,7 +531,7 @@ function* handleUsbConnectPybricks(hotPlugDevice?: USBDevice): Generator {
}

const { response, timeout } = yield* race({
response: take(usbDidReceivePybricksMessageResponse),
response: take(responseChannel),
timeout: delay(1000),
});

Expand Down