Skip to content

Conversation

@jaguilar
Copy link

No description provided.

hub.light.on(Color.RED)
conn = None
try:
conn = await rfcomm_connect(TARGET_BLUETOOTH_ADDRESS)
Copy link
Member

Choose a reason for hiding this comment

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

If we are going to use it like this, we should make the connection object a context manager, so we can simply use a with statement instead of try/finally.

Copy link
Member

Choose a reason for hiding this comment

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

Although I do prefer this functional style of rfcomm_connect() creating a connection object in general, it doesn't match our established pattern of a more object-oriented approach in Pybricks.

I.e. we would have rfcomm = Rfcomm() in the setup phase of the program (address could be here or later). And this object would have connect(), wait_for_connection() and disconnect() (or close()) methods.

Copy link
Author

Choose a reason for hiding this comment

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

R.e. context manager: I agree. I will note it as a TODO before we mark the PR as mergable.

R.e. the functional vs. object style: Initially I was going to submit this as a purely experimental API. But since it seems like this might be something we can actually launch, I will convert this to an object style API as you suggest. It might actually make the context manager a little less complicated, since you can do

with RFCOMMSocket() as sock:
  await sock.connect(...)

As opposed to

async with rfcomm_connect(...) as sock:
  ...

Copy link
Member

Choose a reason for hiding this comment

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

I wasn't saying that we have to do it the object oriented way. But we should consider how this will fit into the block programming language as well.

with RFCOMMSocket() as sock:

This is actually not ideal when used in a loop because it will create a new object on each loop. Maybe I was too quick to suggest using a context manager.

Our usual pattern is to declare all objects globally at the beginning of the program (setup stack in the block language).

Another thing that has come up in the past is a need to set the actual channel being used. E.g. when Windows connects to a Bluetooth device with serial service, it automatically sets up 2 RFCOMM channels, one for incoming and one for outgoing, so we have to put in the right channel number for that.

So my first thought would be to do something like:

hub = EV3Brick()
comm0 = RFCOMMChannel(0)

I'm a little tempted to include the address in the constructor as well, but there are different rules depending on if you are connecting or listening.

Copy link
Author

@jaguilar jaguilar Jan 15, 2026

Choose a reason for hiding this comment

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

This is actually not ideal when used in a loop because it will create a new object on each loop. Maybe I was too quick to suggest using a context manager.

We should expect the need to reconnect to be very rare. I think it's probably okay.

E.g. when Windows connects to a Bluetooth device with serial service, it automatically sets up 2 RFCOMM channels, one for incoming and one for outgoing, so we have to put in the right channel number for that.

I didn't notice that behavior when I was messing with it. I had been under the impression that you can send and receive with the same rfcomm channel. AFAICT when you open a socket with python in Windows you only get one rfcomm channel created on the server. I did also create "ping-pong" test scripts on Windows and both when acting as a server and client, it sends and receives messages on the same rfcomm channel (from btstack's perspective).

I actually have removed myself from Windows lately but I would be interested to hear more about this.

comm0 = RFCOMMChannel(0)

Currently, the code does not allow the user to select which channel to address. This is a problem that we'll want to fix in a later update.

Copy link
Member

Choose a reason for hiding this comment

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

When I said incoming/outgoing, I meant connecting or listening, not sending or receiving.

I don't remember the exact details, but there should be some older issues from Pybricks 2.x on EV3 where people ran into this issue. We hard-coded the channel number to 0 (IIRC?) since that is what LEGO did on the EV3. But that didn't work for some people who were trying to talk to Windows instead of another EV3.

Copy link
Author

Choose a reason for hiding this comment

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

Understood. FWIW, I was able to talk to Windows with this. One drawback of the current code is that it will randomly select among the available channels windows advertises over SDP (picking 1 if possible). It would not be a bad thing to give the users an option of picking a specific channel.

Personally I would advise our users to steer clear of outgoing com ports on Windows. Just use Python with socket(AF_BLUETOOTH) instead, IMO.

Copy link
Member

Choose a reason for hiding this comment

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

Personally I would advise our users to steer clear of outgoing com ports on Windows. Just use Python with socket(AF_BLUETOOTH) instead, IMO.

Sure. Windows is still going to create the COM ports though, which ties up a couple of the RFCOMM channels whether you use the COM ports or not. So if we can make it just work without an explicit channel and avoid the ones Windows uses, all the better. It would be nice if could still talk to NXT/EV3 running official LEGO firmware too though, which might require specifying a specific channel.

Copy link
Author

Choose a reason for hiding this comment

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

Sounds good. I'm down to add specifying a channel explicitly. And likewise on the server.

# higher rate than we send out updates, because
# their motors need to be adjusted more frequently
# than our RC device needs to receive updates.
continue
Copy link
Member

Choose a reason for hiding this comment

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

This needs an await somewhere before here to avoid hogging the run loop.

Also, seems like we could just use await wait(UPDATE_PERIOD - update.time()) if we want a variable delay.

Copy link
Author

Choose a reason for hiding this comment

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

My mistake! I will add that.

timeout = StopWatch()
cur_idx = 0
while timeout.time() < 100:
cur_idx += conn.readinto(msg_buf_view[cur_idx:], len(msg_buf) - cur_idx)
Copy link
Member

Choose a reason for hiding this comment

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

Is this blocking when there is no data available?

I'm guessing yes, which won't work with async programming. If there is no data, it should raise an OSError indicating that it would block.

The way I've envisioned making streams with with async in a simple way is something like this:

while True:
    # available() returns the number of bytes in the read buffer
    # or raise exception if disconnected.
    if conn.available() < 2:
        await wait(0)
        continue

    # The method doesn't have to actually be named this, but the behavior
    # should be that we never allow short reads so that users don't have
    # to deal with that.
    data = conn.read_exactly(2)

    # do something with the data
    ...

This was considering we would be using the MicroPython stream APIs where we couldn't make read/write async.

But really, I don't think there is a reason we would need to do that. So we could make the API simply.

while True:
    # Read exactly 2 bytes, waiting if it isn't available
    # or raise exception if disconnected.
    data = await conn.read(2)

    # Do something with data
    ...

Copy link
Author

@jaguilar jaguilar Jan 15, 2026

Choose a reason for hiding this comment

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

This is actually non-blocking, unlike read_exactly (to be clear read_exactly is an already existing stream protocol function). @laurensvalk and I were talking about adding a read_into_exactly function that would be awaitable. We need to ponder this -- currently it just implements the stream protocol and has the same behavior as the stream protocol.

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.

2 participants