Skip to content

Stay connected to peripherals when program ends#459

Open
laurensvalk wants to merge 13 commits intomasterfrom
stay-connected
Open

Stay connected to peripherals when program ends#459
laurensvalk wants to merge 13 commits intomasterfrom
stay-connected

Conversation

@laurensvalk
Copy link
Member

Fixes:

Ever since we supported the Powered Up remote, peripherals would disconnect when the program ends. This wasn't quite so bad for the Powered Up remote since it generally just stays on while you load a new program.

Other peripherals like the Xbox Controller turn off entirely, and need to stay off a little while before you can turn them back on. This makes iterating code quite annoying.

This was initially due to driver limitations, but the Bluetooth drivers have had a lot of improvements since then.

This PR leverages these changes to allow staying connected, in a way that doesn't change the overal user code or user experience. Users will still initiate their device as my_remote = Remote(). But the second time you connect, this will just pass instantly since it is already connected.

This works for multiple peripherals too. It remembers the advertisement and response that it originally matched to, so a new class can be initiated and re-use an existing connection if the filters match. In the following demo (admittedly a bit silly), you can see this in action. Towards the end, the program is stopped and restarted, and everything just stays connected.

signal-2026-02-03-194159_002.mp4

This PR also added async-compatible connect() and disconnect() and a connect=True kwarg to the class initializer so you can optionally skip connecting right at the beginning.

Do it on shutdown instead, and simplify shutdown logic more generally.
Release user claim if it is not connected and not currently busy connecting.

This allows reconnecting if disconnected due to the peripheral powering off.
Multiple peripherals of the same kind may have a different config. This was still in its singleton form after adding support for multiple peripherals earlier.

With a singleton instance, there would be a conflict for e.g. a Powered Up remote and a Mario Hub.
This allows testing behaviors such as remaning connected to peripherals between program runs.
@laurensvalk
Copy link
Member Author

Still need some cleanup and testing. This has only been tested on Prime Hub so far.

City/Move/Technic support only one peripheral, but auto re-connection has been added (not yet tested).

@BertLindeman
Copy link
Contributor

BertLindeman commented Feb 3, 2026

Never too soon you said ;-)

Short:

  1. starting with xbox = XboxController(connect=False) disconnects if the xbox ctrl already had a connection.
  2. running the xbox connection 3 times in one program makes the program forget that the connection exists.
    With (connect) then (connect=False) all goes well but then (connect=True) tries to (re?)-connect but the xbox still is connected.

Details 1
Status: xbox controller** is connected and then run:

from pybricks.iodevices import XboxController
xbox = XboxController(connect=False)

Should this disconnect the controller of not?

With this firmware it disconnects:
Pybricks MicroPython ci-build-4688-v4.0.0b5-15-g8fd405e4 on 2026-02-03; SPIKE Prime Hub with STM32F413VG

Details 2
[EDIT] Forgot to paste the first program line (empty) but ten the traceback was wrong . . .

from pybricks.iodevices import XboxController
from pybricks.tools import wait

print("pre  simple connect")
xbox = XboxController()
wait(250)
print("pre  connect=False")
xbox = XboxController(connect=False)
wait(250)
print("post connect=False")
xbox = XboxController(connect=True)
wait(250)
print("post connect=True")

for i in range(5):
    print(xbox.dpad())
    wait(500)

print("Done")

Results in:

pre  simple connect
pre  connect=False
post connect=False
Traceback (most recent call last):
  File "test_connect.py", line 12, in <module>
OSError: [Errno 116] ETIMEDOUT: Timed out

The xbox ctrl thinks it is connected the whole time the program runs.
The program thinks on this line xbox = XboxController(connect=True) that there is no connection.

@laurensvalk
Copy link
Member Author

laurensvalk commented Feb 3, 2026

  1. If someone goes out of their way to type connect=False I'd assume they wouldn't want to be connected at the start. Right?

  2. I think this is also the expected behavior, but it's definitely an odd program. The first will connect. The second creates an object that will not connect, but also frees the first. And finally the third attempts to create another instance. It will then look for another controller, but you have only one.

If your goal is to interactively connect and disconnect, you can use the connect and disconnect methods now. They were introduced so you don't have to keep making new objects.

Could you also try the intended scenario? 😄 So just create your usual programs with a remote. You should be able to stop and start programs without hassle of restarting the controller.

@BertLindeman
Copy link
Contributor

BertLindeman commented Feb 3, 2026

f your goal is to interactively connect and disconnect, you can use the connect and disconnect methods now. They were introduced so you don't have to keep making new objects.

Will do

Could you also try the intended scenario? 😄 So just create your usual programs with a remote. You should be able to stop and start programs without hassle of restarting the controller.

Did that and that works fine, both for xbox and remote.
Was just curious...

@laurensvalk
Copy link
Member Author

Good to hear that it works!

Reduce the number of partially parsed arguments that we pass around.

This makes everything simpler, reduces code size, and we can keep the address
out of the filters. This way we can re-use them outside of the scan-and-connect
procedure.
Parse arguments only during init and reset the appropriate state only on reconnect.

This is working towards a real and virtual reconnect.
We want to re-use these for every connect call.
Allow instantiating without connecting, letting the user (asynchronously) connect later.

Fixes pybricks/support#1800
If the timeout was long, resetting it in between the scan and scan response makes it shorter, which is not intended. Keep it simple by extending only for connect and pairing.

Also refactor the waiting loops to make them easier to follow with a scan timeout shorthand.
This lets you stay connected and use your regular code. If you do Remote(args) and a matching device is connected and not already in use, you get this device.
When re-using an existing connection, this lets us make sure all filters match as if making a new connection.
When we skip discovery, we'll still need the handle. There is only one handle for LWP3 devices, so we can reuse it.

We might revisit this to skip only the connection but redo the discovery phase for generality.
@coveralls
Copy link

Coverage Status

coverage: 49.552% (-0.2%) from 49.797%
when pulling 7ff2e5a on stay-connected
into 4849642 on master.

@laurensvalk
Copy link
Member Author

Now tested and fixed for Move/City/Technic Hub.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants