Skip to content

Bluetooth classic RFCOMM communication support#452

Open
jaguilar wants to merge 9 commits intopybricks:masterfrom
jaguilar:ev3-bluetooth-rfcomm
Open

Bluetooth classic RFCOMM communication support#452
jaguilar wants to merge 9 commits intopybricks:masterfrom
jaguilar:ev3-bluetooth-rfcomm

Conversation

@jaguilar
Copy link
Contributor

@jaguilar jaguilar commented Jan 15, 2026

Please ignore the whitespace changes. I'll revert them when I get a chance. They weren't intended.

This has been tested with the example programs in pybricks/pybricks-projects#100

I expect some changes will be required for this pull request, but I'm putting it out there to demonstrate I have something working and as a jumping off point for further discussion.

(We will also need to decide exactly what functionality will be #ifdef'd out. If we've decided to put classic on all of the hubs that use btstack we could just remove some ifdefs and things would build.)

TODO

  • Revert unnecessary whitespace changes/fix formatting.
  • Add socket resets to soft reset when user program ends.
  • Add cancellation support.
  • Create RFCOMMSocket object and add context manager support.
  • General cleanup.
  • Consider moving to the style where we process the events leading up to connection linearly inside connection process functions, similar to inquiry scan? Per discussion with @laurensvalk we will defer this if we do it at all.
  • Consider changing python API to support asyncio-like awaitable read and write commands? Or expose asyncio?
  • Migrate to UARTDevice API.
  • Fix build/decide what to do about ifdefs.
  • Separate RFCOMM event handling from HCI event handler, and migrate classic security-related HCI events into main HCI event handler.
  • Fix usage of gap_connectable_control(), which is currently incorrect.

@BertLindeman
Copy link
Contributor

James,

Trying my EV3 with this firmware on the tankbot_rc
The program runs up to the rfcomm_listen and waits.
Log:

Local address:  A0:E6:F8:E4:42:36
Waiting for connection...
Loaded 0 link keys from settings
[btc:rfcomm_listen] Listening for incoming RFCOMM connections...
sm.c.720: GAP Random Address Update due
sm.c.711: gap_random_address_trigger, state 0
HCI out packet type: 01, len: 3
HCI in packet type: 04, len: 2
HCI in packet type: 04, len: 6
HCI out packet type: 01, len: 9
HCI in packet type: 04, len: 2
HCI in packet type: 04, len: 6
The program was stopped (SystemExit).

What kind of device did you use as RC?
Or do I need 2 EV3brcks for this?

Bert

@jaguilar
Copy link
Contributor Author

jaguilar commented Jan 15, 2026

Hi, @BertLindeman! So kind of you to try this. You do need some sort of client to talk to the tankbot, be it another EV3 or a PC. If you have two EV3s, the client program here is designed to implement this LEGO model. If you run the program on another brick it should connect.

If you want to try connecting from a computer, you'll need a computer with bluetooth. I believe you should be able to connect in Python with something like:

import socket
sock = socket.socket(family=socket.AF_BLUETOOTH)
await sock.connect('XX:XX:XX:XX:XX:XX') # The address printed on the terminal by the tankbot.

You would send messages to the socket that are packed with the struct module -- the code is very short so you should be able to see the desired format.

@jaguilar jaguilar force-pushed the ev3-bluetooth-rfcomm branch 3 times, most recently from 23477a9 to 29f1254 Compare January 15, 2026 20:34
@jaguilar
Copy link
Contributor Author

FYI, this is building fine locally. Not sure what the deal is with CI. Possibly because the build state is not good at certain intermediate commits. I can squash whenever it is so desired.

@BertLindeman
Copy link
Contributor

Used firmware ('ev3', '4.0.0b3', 'ci-build-4625-v4.0.0b3-114-gab4df866 on 2026-01-15')
on Windows 11 25H2.

With EV3 program:#!/usr/bin/env pybricks-micropython # simple tankbot program to test Bluetooth classic connection from pybricks.hubs import EV3Brick from pybricks.ev3devices import Motor, GyroSensor from pybricks.parameters import Port, Direction, Button, Color from pybricks.tools import StopWatch, wait, run_task from pybricks.robotics import DriveBase from pybricks.messaging import rfcomm_listen, local_address from micropython import const import ustruct from pybricks import version # Initialize the EV3 brick. ev3 = EV3Brick() print(version) left_motor = Motor(Port.A, Direction.COUNTERCLOCKWISE) right_motor = Motor(Port.D, Direction.COUNTERCLOCKWISE) WHEEL_DIAMETER = 54 AXLE_TRACK = 200 robot = DriveBase(left_motor, right_motor, WHEEL_DIAMETER, AXLE_TRACK) SPEED_SCALE = 6 TURN_SCALE = 2 # Storage for incoming messages from remote control. msg_buf = bytearray(2) msg_buf_view = memoryview(msg_buf) # Tracks the next-to-be-filled index in msg_buf. cur_idx = 0 # keep the local address out of the loop. It will not change. addr = local_address() print("Local address:", addr) async def main(): # light red on listening for a connection ev3.light.on(Color.RED) print('Waiting for connection...') try: conn = await rfcomm_listen() print('Connected!') # light green on connection ev3.light.on(Color.GREEN) except KeyboardInterrupt: conn.close() wait(200) raise SystemExit(0) timeout = StopWatch() cur_idx = 0 try: while timeout.time() < 2000: # Do not kame the wait too short cur_idx += conn.readinto(msg_buf_view[cur_idx:], len(msg_buf) - cur_idx) if cur_idx != len(msg_buf): # We were not able to read the entire message. Loop again. await wait(1) continue timeout.reset() axis1, axis2 = ustruct.unpack('>bb', msg_buf) cur_idx = 0 if axis1 == axis2 == -127: # stop connection print("\tGot stop signal from client") break speed = axis2 * SPEED_SCALE # -768 to +768 mm/s turn_rate = axis1 * TURN_SCALE # -320 to +320 deg/s robot.drive(speed, turn_rate) except OSError: print("\tRFCOMM error") except KeyboardInterrupt: print("\tGot keyboard interrupt") pass finally: robot.stop() # make sure to socket and channel are cleaned up conn.close() wait(200) # wait a bit to give the close some time. print('\tIn finally: Closed connection') run_task(main())
And Windows program:
import socket
import struct
import time

ADDR = "A0:E6:F8:E4:42:36"
CHANNEL = 1

def get_key():
    import msvcrt
    if msvcrt.kbhit():
        return msvcrt.getch().decode("ascii").lower()
    return None

sock = socket.socket(
    socket.AF_BLUETOOTH,
    socket.SOCK_STREAM,
    socket.BTPROTO_RFCOMM
)

STD_MOTOR_SCALE = 20
try:
    sock.connect((ADDR, CHANNEL))
    sock.settimeout(2.0)
    print("Connected. W/S/A/D to drive, space=stop, Q=quit")

    axis1 = 0  # turn
    axis2 = 0  # speed

    while True:
        key = get_key()

        if key == 'w':
            axis2 = STD_MOTOR_SCALE
        elif key == 's':
            axis2 = -STD_MOTOR_SCALE
        elif key == 'a':
            axis1 = -STD_MOTOR_SCALE
        elif key == 'd':
            axis1 = STD_MOTOR_SCALE
        elif key == ' ':
            axis1 = 0
            axis2 = 0
        elif key == 'q':
            axis1 = -127  # signal stop run
            axis1 = -127
            msg = struct.pack('>bb', axis1, axis2)
            sock.send(msg)
            break

        # ALWAYS send, even if unchanged
        msg = struct.pack('>bb', axis1, axis2)
        sock.send(msg)

        time.sleep(0.05)

except OSError as e:
    print("RFCOMM error:", e)

finally:
    try:
        sock.close()
    except OSError:
        pass
    print("Disconnected")
Log of one run: ``` pybricksdev run usb ..\EV3\tankbot_rc.py 100%|██████████████████████████████████████████████████████████████████████████████████████████████████| 1.33k/1.33k [00:00<00:00, 432kB/s] ('ev3', '4.0.0b3', 'ci-build-4625-v4.0.0b3-114-gab4df866 on 2026-01-15') Local address: A0:E6:F8:E4:42:36 Waiting for connection... [btc:rfcomm_listen] Listening for incoming RFCOMM connections... hci.c.3431: Connection_incoming: 8C:90:2D:41:E4:60, type 1 hci.c.312: create_connection_for_addr 8C:90:2D:41:E4:60, type fd hci.c.6672: sending hci_accept_connection_request hci.c.3457: Connection_complete (status=0) 8C:90:2D:41:E4:60 hci.c.3494: New connection: handle 1, 8C:90:2D:41:E4:60 hci.c.7505: BTSTACK_EVENT_NR_CONNECTIONS_CHANGED 1 IDENTITY_RESOLVING_STARTED sm.c.2269: LE Device Lookup: not found IDENTITY_RESOLVING_FAILED hci.c.506: pairing started, ssp 1, initiator 0, requested level 2 hci.c.7677: gap_mitm_protection_required_for_security_level 2 hci.c.6736: Remote not bonding, dropping local flag SSP User Confirmation Request. Auto-accepting... hci.c.528: pairing complete, status 00 Link key updated, saving to settings. Saved 0 link keys to settings sm.c.3900: Encryption state change: 1, key size 0 sm.c.3902: event handler, state 82 hci.c.2788: Handle 0001 key Size: 16 hci.c.7536: hci_emit_security_level 2 for handle 1 l2cap.c.2833: security level update for handle 0x0001 l2cap.c.3633: extended features mask 0xba l2cap.c.2460: create channel c007e770, local_cid 0x0042 l2cap.c.3649: fixed channels mask 0x8a hci.c.2509: Remote features 03, bonding flags 70 l2cap.c.2822: remote supported features, channel c007e770, cid 0042 - state 4 l2cap.c.1159: L2CAP_EVENT_INCOMING_CONNECTION addr 8C:90:2D:41:E4:60 handle 0x1 psm 0x3 local_cid 0x42 remote_cid 0x40 rfcomm.c.382: rfcomm_max_frame_size_for_l2cap_mtu: 1691 -> 1686 rfcomm.c.1074: RFCOMM incoming (l2cap_cid 0x42) => accept l2cap.c.3131: L2CAP_ACCEPT_CONNECTION local_cid 0x42 l2cap.c.1404: l2cap_stop_rtx for local cid 0x42 l2cap.c.1441: l2cap_start_rtx for local cid 0x42 l2cap.c.3359: L2CAP signaling handler code 4, state 11 l2cap.c.3187: Remote MTU 1017 l2cap.c.3359: L2CAP signaling handler code 5, state 11 l2cap.c.1404: l2cap_stop_rtx for local cid 0x42 l2cap.c.3289: l2cap_signaling_handle_configure_response l2cap.c.1129: L2CAP_EVENT_CHANNEL_OPENED status 0x0 addr 8C:90:2D:41:E4:60 handle 0x1 psm 0x3 local_cid 0x42 remote_cid 0x40 local_mtu 1691, remote_mtu 1017, flush_timeout 0 rfcomm.c.1101: channel opened, status 0 rfcomm.c.382: rfcomm_max_frame_size_for_l2cap_mtu: 1691 -> 1686 rfcomm.c.1219: Received SABM #0 rfcomm.c.1364: Sending UA #0 rfcomm.c.941: Multiplexer up and running rfcomm.c.1640: Received UIH Parameter Negotiation Command for #2, credits 7 rfcomm.c.509: rfcomm_channel_create for service c007e44c, channel 1 --- list of channels: rfcomm.c.1997: -> Inform app rfcomm.c.247: RFCOMM_EVENT_INCOMING_CONNECTION addr 8C:90:2D:41:E4:60 channel #1 cid 0x02 rfcomm.c.2628: accept cid 0x02 rfcomm.c.2025: Sending UIH Parameter Negotiation Respond for #2 rfcomm.c.1782: rfcomm_channel_ready_for_incoming_dlc_setup state var 00000003 rfcomm.c.1607: Received SABM #2 rfcomm.c.2029: Sending UA #2 rfcomm.c.1782: rfcomm_channel_ready_for_incoming_dlc_setup state var 00000007 rfcomm.c.2034: Incomping setup done, requesting send MSC CMD and send Credits rfcomm.c.1942: Sending MSC CMD for #2 rfcomm.c.2123: Providing credits for #2 rfcomm.c.1773: rfcomm_channel_ready_for_open state 8, flags needed 0010, current 00014007, rf credits 7 rfcomm.c.1660: Received MSC CMD for #2, rfcomm.c.1773: rfcomm_channel_ready_for_open state 8, flags needed 0010, current 0001500f, rf credits 7 rfcomm.c.1949: Sending MSC RSP for #2 rfcomm.c.1667: Received MSC RSP for #2 rfcomm.c.1773: rfcomm_channel_ready_for_open state 8, flags needed 0010, current 0001c01f, rf credits 7 rfcomm.c.1410: opened rfcomm.c.265: RFCOMM_EVENT_CHANNEL_OPENED status 0x0 addr 8C:90:2D:41:E4:60 handle 0x1 channel #1 cid 0x02 mtu 1011 RFCOMM channel opened: cid=2. rfcomm.c.2667: grant cid 0x02 credits 4 [btc:rfcomm_listen] Connected Connected! rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 25, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.1447: RFCOMM data UIH_PF, new credits channel 0x02: 0, now 32 rfcomm.c.2667: grant cid 0x02 credits 2 rfcomm.c.291: RFCOMM_EVENT_CHANNEL_CLOSED cid 0x02 RFCOMM_EVENT_CHANNEL_CLOSED by remote for cid=2. RFCOMM channel closed: cid=2. rfcomm.c.2214: Sending UA after DISC for #2 [btc:rfcomm_recv] Socket is not connected or does not exist. RFCOMM error rfcomm.c.2553: disconnect cid 0x02 In finally: Closed connection rfcomm.c.1237: Received DISC #0, (ougoing = 0) rfcomm.c.1371: Sending UA #0 rfcomm.c.1372: Closing down multiplexer l2cap.c.3359: L2CAP signaling handler code 6, state 13 l2cap.c.1188: L2CAP_EVENT_CHANNEL_CLOSED local_cid 0x42 rfcomm.c.1174: channel closed cid 0x42, mult 0 l2cap.c.2466: free channel c007e770, local_cid 0x0042 l2cap.c.1404: l2cap_stop_rtx for local cid 0x42 ```

Fun.
I had to add the finally to the EV3 program to prevent already listening on a program restart.
Or I needed to reboot the EV3 to get rid of the socket? connection? or channel?

@jaguilar
Copy link
Contributor Author

Bert, did you still have to add the finally with 29f1254, or was that with the firmware you built earlier? The fixup commits that I added were meant to fix that issue, but I guess I never tried it without removing the finally from my own test scripts.

@BertLindeman
Copy link
Contributor

Bert, did you still have to add the finally with 29f1254, or was that with the firmware you built earlier? The fixup commits that I added were meant to fix that issue, but I guess I never tried it without removing the finally from my own test scripts.

James,
The finally is needed using ('ev3', '4.0.0b3', 'ci-build-4625-v4.0.0b3-114-gab4df866 on 2026-01-15') from the CI builds.
I did no builds myself.
Before that firmware I did not succeed as my programs were no good yet.
Do you want me to try another CI build?

@BertLindeman
Copy link
Contributor

Just for my understanding:

If the user program stops after
conn = await rfcomm_listen()

Then the next run of the program fails with [btc:rfcomm_listen] Already listening.:

pybricksdev run usb  ..\EV3\tankbot_rc.py
100%|██████████████████████████████████████████████████████████████████████████████████████████████████| 1.29k/1.29k [00:00<00:00, 403kB/s]
('ev3', '4.0.0b3', 'ci-build-4625-v4.0.0b3-114-gab4df866 on 2026-01-15')
Local address: A0:E6:F8:E4:42:36
Waiting for connection...
[btc:rfcomm_listen] Already listening.
Traceback (most recent call last):
  File "tankbot_rc.py", line 99, in <module>
  File "tankbot_rc.py", line 50, in main
OSError: [Errno 16] EBUSY: Device or resource busy

Is that the expected result, so the python program should use e.g. a finally clause to stop the listening?

@jaguilar
Copy link
Contributor Author

No, that is not the expected result. I will have to go back and test my fixes in the latest set of commits. They are intended to clean up all of the outstanding sockets, but it may be that I made a mistake.

@jaguilar jaguilar force-pushed the ev3-bluetooth-rfcomm branch from 29f1254 to 0b1bc47 Compare January 18, 2026 17:28
@jaguilar
Copy link
Contributor Author

jaguilar commented Jan 18, 2026

I investigated how these APIs are typically implemented in Micropython. Particularly usocket. It looks like the general pattern is to implement things as objects. Therefore, I converted messaging to have an RFCOMMSocket object, which is what @dlech originally suggested.

I updated the examples to show what the API looks like now.

@BertLindeman I have tested the latest firmware (through 43ef418) and verified that if you disconnect the remote and reconnect it, you no longer get already-in-use errors, even without the try/finally. Please let me know if you find otherwise.

@jaguilar
Copy link
Contributor Author

One minor point where I'd like to request feedback. Previously, upon closing the RFCOMM socket, we called gap_disconnect. This wasn't correct -- at a minimum, you need to rfcomm disconnect first to get a graceful channel shutdown.

The current RFCOMM code does not call gap_disconnect at all. Our btstack implementation does not count references to the HCI connection layer. If there are two connections to the same remote (e.g. both an LE connection and an RFCOMM connection, or two separate RFCOMM connections), gap_disconnect would terminate all of them, rather than just the specific RFCOMM socket that is being closed. It's not safe to do.

The problem with the current approach is that without gap_disconnect, the radio link stays up. It is kinda convenient, because when you restart an RFCOMM client program after a crash, it will connect basically instantly, skipping the slow classic link establishment process. However, because the radio link is still up, the brick is consuming a significant amount of power that it would not be doing if all of the radio links were closed down.

It might not be that big of an issue, but if it is, we need to start counting references to the HCI connections and calling gap_disconnect when the last reference is removed. Please let me know if you think that's important to do.

@BertLindeman
Copy link
Contributor

James,

Ran your tankbot_rc on the now latest firmware:
('ev3', '4.0.0b3', 'ci-build-4628-v4.0.0b3-118-g05947152 on 2026-01-18')

log of one run until the EV3 is listening and then stop the program using the EV3 back button.
No re-boot done here.
The following run then fails "Already listening":

C:\Users\bert\py\pybricks>pybricksdev run usb  ..\EV3\tankbot_rc_james.py
rted 14/7
hci.c.2593: Command 0x03 supported 18/3
hci.c.2593: Command 0x04 supported 20/4
hci.c.2593: Command 0x06 supported 24/6
hci.c.2598: Local supported commands summary 0000005f
btstack_crypto.c.1121: controller supports ECDH operation: 0
hci.c.2722: Local Address, Status: 0x00: Addr: A0:E6:F8:E4:42:36
hci.c.2640: hci_read_buffer_size: ACL size module 1021 -> used 1021, count 4 / SCO size 180, count 4
hci.c.2756: Packet types cc18, eSCO 1
hci.c.2759: BR/EDR support 1, LE support 0
100%|██████████████████████████████████████████████████████████████████████████████████████████████████| 1.15k/1.15k [00:00<00:00, 383kB/s]
hci.c.1770:('ev3', '4.0.0b3', 'ci-build-4628-v4.0.0b3-118-g05947152 on 2026-01-18')
Local address:  A0:E6:F8:E4:42:36
Waiting for connection...
Loaded 0 link keys from settings
[btc:rfcomm_listen] Listening for incoming RFCOMM connections...
The program was stopped (SystemExit).

C:\Users\bert\py\pybricks>pybricksdev run usb  ..\EV3\tankbot_rc_james.py
100%|██████████████████████████████████████████████████████████████████████████████████████████████████| 1.15k/1.15k [00:00<00:00, 574kB/s]
('ev3', '4.0.0b3', 'ci-build-4628-v4.0.0b3-118-g05947152 on 2026-01-18')
Local address:  A0:E6:F8:E4:42:36
Waiting for connection...
[btc:rfcomm_listen] Already listening.
Traceback (most recent call last):
  File "tankbot_rc_james.py", line 89, in <module>
  File "tankbot_rc_james.py", line 63, in main
OSError: [Errno 16] EBUSY: Device or resource busy

As I only have one EV3, so I need to convert my windows program a bit, so later....

Bert

@jaguilar
Copy link
Contributor Author

Hrm. That is very interesting. Let me try it with exactly the sequence that you are using incl the client program on PC and see if I can repro.

@laurensvalk laurensvalk marked this pull request as ready for review January 18, 2026 21:04
@laurensvalk
Copy link
Member

Never mind my config change to this PR - that wasn't intentional. The mobile app doesn't seem to have a way of undoing it. Was just trying to catch up 😄

@jaguilar
Copy link
Contributor Author

@BertLindeman I'm sorry, I can't reproduce your issue from my Linux machine. When I run the tank_bot_rc/main.py program again (without rebooting) it listens just fine. I'm wondering if the CI build is actually incorporating all the commits. @laurensvalk do you know if there is a way to check that?

@BertLindeman
Copy link
Contributor

@jaguilar There was no other program involved, just:

  • run the tank_rc program
  • press the EV3 back button
  • run the tank_rc program
    and get "already listening"
the `tankbot_rc_james.py` program I used
#!/usr/bin/env pybricks-micropython

"""
Example LEGO® MINDSTORMS® EV3 Tank Bot Program
----------------------------------------------

This program requires LEGO® EV3 MicroPython v2.0.
Download: https://education.lego.com/en-us/support/mindstorms-ev3/python-for-ev3

Building instructions can be found at:
https://education.lego.com/en-us/support/mindstorms-ev3/building-instructions#building-expansion
"""

from pybricks.hubs import EV3Brick
from pybricks.ev3devices import Motor, GyroSensor
from pybricks.parameters import Port, Direction, Button, Color
from pybricks.tools import StopWatch, wait, run_task
from pybricks.robotics import DriveBase
from pybricks.messaging import RFCOMMSocket, local_address 

from micropython import const

import ustruct
from pybricks import version

# Initialize the EV3 brick.
ev3 = EV3Brick()
print("\t", version, "\n")

# Configure 2 motors on Ports B and C.  Set the motor directions to
# counterclockwise, so that positive speed values make the robot move
# forward.  These will be the left and right motors of the Tank Bot.
left_motor = Motor(Port.A, Direction.COUNTERCLOCKWISE)
right_motor = Motor(Port.D, Direction.COUNTERCLOCKWISE)

# The wheel diameter of the Tank Bot is about 54 mm.
WHEEL_DIAMETER = 54

# The axle track is the distance between the centers of each of the
# wheels.  This is about 200 mm for the Tank Bot.
AXLE_TRACK = 200

# The Driving Base is comprised of 2 motors.  There is a wheel on each
# motor.  The wheel diameter and axle track values are used to make the
# motors move at the correct speed when you give a drive command.
robot = DriveBase(left_motor, right_motor, WHEEL_DIAMETER, AXLE_TRACK)

SPEED_SCALE = 6  # Scale factor for speed (768 // 127)
TURN_SCALE = 2  # Scale factor for turn rate (320 // 127)

# Storage for incoming messages from remote control.
msg_buf = bytearray(2)    
msg_buf_view = memoryview(msg_buf)

# Tracks the next-to-be-filled index in msg_buf.
cur_idx = 0

async def main():
    sock = RFCOMMSocket()
    print('Local address: ', local_address())
    ev3.light.on(Color.RED)
    print('Waiting for connection...')
    await sock.listen()
    print('Connected!')
    ev3.light.on(Color.GREEN)

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

        if cur_idx != len(msg_buf):
            # We were not able to read the entire message. Loop again.
            await wait(1)
            continue

        timeout.reset()
        axis1, axis2 = ustruct.unpack('>bb', msg_buf)
        cur_idx = 0

        speed = axis2 * SPEED_SCALE  # -768 to +768 mm/s
        turn_rate = axis1 * TURN_SCALE  # -320 to +320 deg/s
        robot.drive(speed, turn_rate)

    robot.stop()
    print('Client disconnected or timed out.')
    sock.close()

run_task(main())

@jaguilar
Copy link
Contributor Author

I understand now. i can reproduce. I'll figure it out!

@jaguilar
Copy link
Contributor Author

Should be fixed after latest commit.

@BertLindeman
Copy link
Contributor

tomorrow for me. You are quick. Thanks!

@jaguilar
Copy link
Contributor Author

jaguilar commented Jan 19, 2026

Complaints about CI

Nevermind, forgot that CI builds each commit.

@jaguilar jaguilar force-pushed the ev3-bluetooth-rfcomm branch from fbba1f5 to fb7c900 Compare January 19, 2026 06:06
@BertLindeman
Copy link
Contributor

Should be fixed after latest commit.

Fixed this scenario:

  • run the tank_rc program
  • press the EV3 back button
  • run the tank_rc program
    and get "already listening"

Log:

C:\Users\bert\py\pybricks>pybricksdev run usb  ..\EV3\tankbot_rc_james.py
{'pybricks.hubs', 'pybricks.ev3devices', 'pybricks.messaging', 'pybricks.parameters', 'ustruct', 'pybricks.robotics', 'pybricks.tools', 'pybricks', 'micropython'}
{'pybricks.hubs', 'pybricks.ev3devices', 'pybricks.messaging', 'pybricks.parameters', 'ustruct', 'micropython', 'pybricks.robotics', 'pybricks.tools', 'pybricks'}
100%|██████████████████████████████████████████████████████████████████████████████████████████████████| 1.16k/1.16k [00:00<00:00, 582kB/s]
         ('ev3', '4.0.0b3', 'ci-build-4633-v4.0.0b3-110-gcddb1c86 on 2026-01-19')

Local address:  A0:E6:F8:E4:42:36
Waiting for connection...
Loaded 0 link keys from settings
[btc:rfcomm_listen] Listening for incoming RFCOMM connections...
The program was stopped (SystemExit).

C:\Users\bert\py\pybricks>pybricksdev run usb  ..\EV3\tankbot_rc_james.py
{'pybricks.robotics', 'pybricks', 'pybricks.messaging', 'pybricks.hubs', 'pybricks.tools', 'micropython', 'pybricks.parameters', 'ustruct', 'pybricks.ev3devices'}
{'pybricks.robotics', 'pybricks', 'pybricks.messaging', 'pybricks.hubs', 'pybricks.tools', 'micropython', 'pybricks.parameters', 'ustruct', 'pybricks.ev3devices'}
100%|██████████████████████████████████████████████████████████████████████████████████████████████████| 1.16k/1.16k [00:00<00:00, 386kB/s]
         ('ev3', '4.0.0b3', 'ci-build-4633-v4.0.0b3-110-gcddb1c86 on 2026-01-19')

Local address:  A0:E6:F8:E4:42:36
Waiting for connection...
[btc:rfcomm_listen] Listening for incoming RFCOMM connections...
The program was stopped (SystemExit).

@jaguilar
Copy link
Contributor Author

As requested, migrated to use a UARTDevice workalike API. I ran brief tests with my remote control models and everything appears to still be working.

@BertLindeman
Copy link
Contributor

Now testing with ('ev3', '4.0.0b3', 'ci-build-4634-v4.0.0b3-112-g068df580 on 2026-01-20')

  1. I assume a test on a primehub is too soon?
  2. With this commit we dropped the readinto Is that as intended?
    It seemed easy to use.

@jaguilar
Copy link
Contributor Author

@BertLindeman Sorry for the trouble. I've pushed a new commit to the pybricks-projects repo that shows how to use the reformulated API. The new API is actually simpler to use than what was there before, although it does create a bit more garbage.

@BertLindeman
Copy link
Contributor

BertLindeman commented Jan 21, 2026

@BertLindeman Sorry for the trouble. I've pushed a new commit to the pybricks-projects repo that shows how to use the reformulated API. The new API is actually simpler to use than what was there before, although it does create a bit more garbage.

No problem at all.
Code looks much simpler indeed.

Let me know if you think a basic test on a primehub makes sense.

[EDIT]
On firmware ('ev3', '4.0.0b3', 'ci-build-4638-v4.0.0b3-113-g9add495b on 2026-01-21')
Running tankbot_rc.py on the EV3 I get
AttributeError: 'RFCOMMSocket' object has no attribute 'close' on line 75 sock.close()

If I do a dir(RFCOMMSocket) it does not show close.

Complete log of the tankbot_rc run
C:\Users\bert\py\pybricks\issue_2274_rfcomm>pybricksdev run usb tankbot_rc.py
rted 14/7
hci.c.2593: Command 0x03 supported 18/3
hci.c.2593: Command 0x04 supported 20/4
hci.c.2593: Command 0x06 supported 24/6
hci.c.2598: Local supported commands summary 0000005f
btstack_crypto.c.1121: controller supports ECDH operation: 0
hci.c.2722: Local Address, Status: 0x00: Addr: A0:E6:F8:E4:42:36
hci.c.2640: hci_read_buffer_size: ACL size module 1021 -> used 1021, count 4 / SCO size 180, count 4
hci.c.2756: Packet types cc18, eSCO 1
hci.c.2759: BR/EDR support 1, LE support 0
{'pybricks.ev3devices', 'pybricks.tools', 'pybricks.messaging', 'pybricks.robotics', 'ustruct', 'micropython', 'pybricks.hubs', 'pybricks.parameters'}
{'pybricks.ev3devices', 'pybricks.tools', 'pybricks.messaging', 'pybricks.robotics', 'ustruct', 'micropython', 'pybricks.hubs', 'pybricks.parameters'}
100%|██████████████████████████████████████████████████████████████████████████████████████████████████| 1.00k/1.00k [00:00<00:00, 501kB/s]
hci.c.1770:Local address:  A0:E6:F8:E4:42:36
Waiting for connection...
Loaded 0 link keys from settings
[btc:rfcomm_listen] Listening for incoming RFCOMM connections...
hci.c.3431: Connection_incoming: 8C:90:2D:41:E4:60, type 1
hci.c.312: create_connection_for_addr 8C:90:2D:41:E4:60, type fd
hci.c.6672: sending hci_accept_connection_request
hci.c.3457: Connection_complete (status=0) 8C:90:2D:41:E4:60
hci.c.3494: New connection: handle 1, 8C:90:2D:41:E4:60
hci.c.7505: BTSTACK_EVENT_NR_CONNECTIONS_CHANGED 1
IDENTITY_RESOLVING_STARTED
sm.c.2269: LE Device Lookup: not found
IDENTITY_RESOLVING_FAILED
hci.c.506: pairing started, ssp 1, initiator 0, requested level 2
hci.c.7677: gap_mitm_protection_required_for_security_level 2
hci.c.6736: Remote not bonding, dropping local flag
SSP User Confirmation Request. Auto-accepting...
hci.c.528: pairing complete, status 00
Link key updated, saving to settings.
Saved 0 link keys to settings
sm.c.3900: Encryption state change: 1, key size 0
sm.c.3902: event handler, state 82
hci.c.2788: Handle 0001 key Size: 16
hci.c.7536: hci_emit_security_level 2 for handle 1
l2cap.c.2833: security level update for handle 0x0001
l2cap.c.3633: extended features mask 0xba
l2cap.c.3649: fixed channels mask 0x8a
l2cap.c.2460: create channel c007e754, local_cid 0x0041
hci.c.2509: Remote features 03, bonding flags 70
l2cap.c.2822: remote supported features, channel c007e754, cid 0041 - state 4
l2cap.c.1159: L2CAP_EVENT_INCOMING_CONNECTION addr 8C:90:2D:41:E4:60 handle 0x1 psm 0x3 local_cid 0x41 remote_cid 0x40
rfcomm.c.382: rfcomm_max_frame_size_for_l2cap_mtu:  1691 -> 1686
rfcomm.c.1074: RFCOMM incoming (l2cap_cid 0x41) => accept
l2cap.c.3131: L2CAP_ACCEPT_CONNECTION local_cid 0x41
l2cap.c.1404: l2cap_stop_rtx for local cid 0x41
l2cap.c.1441: l2cap_start_rtx for local cid 0x41
l2cap.c.3359: L2CAP signaling handler code 4, state 11
l2cap.c.3187: Remote MTU 1017
l2cap.c.3359: L2CAP signaling handler code 5, state 11
l2cap.c.1404: l2cap_stop_rtx for local cid 0x41
l2cap.c.3289: l2cap_signaling_handle_configure_response
l2cap.c.1129: L2CAP_EVENT_CHANNEL_OPENED status 0x0 addr 8C:90:2D:41:E4:60 handle 0x1 psm 0x3 local_cid 0x41 remote_cid 0x40 local_mtu 1691, remote_mtu 1017, flush_timeout 0
rfcomm.c.1101: channel opened, status 0
rfcomm.c.382: rfcomm_max_frame_size_for_l2cap_mtu:  1691 -> 1686
rfcomm.c.1219: Received SABM #0
rfcomm.c.1364: Sending UA #0
rfcomm.c.941: Multiplexer up and running
rfcomm.c.1640: Received UIH Parameter Negotiation Command for #2, credits 7
rfcomm.c.509: rfcomm_channel_create for service c007e430, channel 1 --- list of channels:
rfcomm.c.1997: -> Inform app
rfcomm.c.247: RFCOMM_EVENT_INCOMING_CONNECTION addr 8C:90:2D:41:E4:60 channel #1 cid 0x01
rfcomm.c.2628: accept cid 0x01
rfcomm.c.2025: Sending UIH Parameter Negotiation Respond for #2
rfcomm.c.1782: rfcomm_channel_ready_for_incoming_dlc_setup state var 00000003
rfcomm.c.1607: Received SABM #2
rfcomm.c.2029: Sending UA #2
rfcomm.c.1782: rfcomm_channel_ready_for_incoming_dlc_setup state var 00000007
rfcomm.c.2034: Incomping setup done, requesting send MSC CMD and send Credits
rfcomm.c.1942: Sending MSC CMD for #2
rfcomm.c.2123: Providing credits for #2
rfcomm.c.1773: rfcomm_channel_ready_for_open state 8, flags needed 0010, current 00014007, rf credits 7
rfcomm.c.1660: Received MSC CMD for #2,
Received unknown RFCOMM event: 135
rfcomm.c.1773: rfcomm_channel_ready_for_open state 8, flags needed 0010, current 0001500f, rf credits 7
rfcomm.c.1949: Sending MSC RSP for #2
rfcomm.c.1667: Received MSC RSP for #2
rfcomm.c.1773: rfcomm_channel_ready_for_open state 8, flags needed 0010, current 0001c01f, rf credits 7
rfcomm.c.1410: opened
rfcomm.c.265: RFCOMM_EVENT_CHANNEL_OPENED status 0x0 addr 8C:90:2D:41:E4:60 handle 0x1 channel #1 cid 0x01 mtu 1011
RFCOMM channel opened: cid=1.
rfcomm.c.2667: grant cid 0x01 credits 4
Received unknown RFCOMM event: 136
[btc:rfcomm_listen] Connected
Connected!
Client disconnected or timed out.
Traceback (most recent call last):
  File "tankbot_rc.py", line 75, in <module>
  File "tankbot_rc.py", line 73, in main
AttributeError: 'RFCOMMSocket' object has no attribute 'close'
rfcomm.c.2553: disconnect cid 0x01
rfcomm.c.1613: Received UA #2
rfcomm.c.291: RFCOMM_EVENT_CHANNEL_CLOSED cid 0x01

@jaguilar
Copy link
Contributor Author

I think it would be interesting -- I'm going to work up a quick test script that exercises the whole class a little more thoroughly, including simultaneous communication on the channel and some throughput and latency tests. After I put that up it might be fun to run one copy of the test script on a spike and the other copy on an EV3 and see how it goes.

@BertLindeman
Copy link
Contributor

Did you notice the missing close method, also on the EV3?
Or is a del sock the future?

Looking forward to the tests.

@jaguilar
Copy link
Contributor Author

I didn't notice it. I'll make sure to test that functionality in the test and then we'll be more assured of catching this in the future. In the mean time, if you do

with RFCOMMSocket() as sock:
  ...

It will have the same effect as close() when you exit the scope.

@BertLindeman
Copy link
Contributor

I didn't notice it. I'll make sure to test that functionality in the test and then we'll be more assured of catching this in the future. In the mean time, if you do

with RFCOMMSocket() as sock:
  ...

It will have the same effect as close() when you exit the scope.

Better coding technique also. Thanks.

@jaguilar
Copy link
Contributor Author

close() added back in the latest fixup commit.

Testing

Try using one brick to run tests/rfcomm_test_follower.py, and then note the bd address. Edit tests/rfcomm_test_leader.py to use the bdaddr from the follower, and then run it on another brick. You should see output like this:

Leader starting.
My Address: 24:71:89:5A:03:23
Connecting to F0:45:DA:13:1C:8A...
Connected.

Test 1: Message Order
  Sending 5 messages...
  Reading messages...
  Packet 1: OK
  Packet 2: OK
  Packet 3: OK
  Packet 4: OK
  Packet 5: OK

Test 2: Throughput
  Packet size: 1024 (Header: 3 + Payload: 1021)
  Result: 93 KB/s (Sent: 93, Recv: 93)

Test 3: RTT (40 samples)
  RTT Mean: 11.38 ms
  RTT 90% CI: +/- 0.18 ms

Test 4: Flow Control
  Sending BLOCK for 500ms
  Bytes sent during 500ms block window: 5120
  Flow Control: PASS

Test 5: waiting and read_all
  waiting: PASS
  read_all: PASS

Test 6: wait_until
  wait_until: PASS

Test 7: clear
  clear: PASS
Leader finished.

@jaguilar
Copy link
Contributor Author

I think there are a few open questions where we need a decision about what to do from the maintainers.

  • Up 'til now, we've been trying to keep all the btstack bluetooth code in one file. In light of this PR, do we want to continue with that? If not, what unit of splitting would you like. I can split the following components:
    • The RFCOMM socket system.
    • The bluetooth classic handlers.
    • The link key database.
    • Any combination of the above.
  • Do you feel that more testing is needed? Is the API adequate to be merged? I feel that this is tested about as well as most things in pybricks but I would welcome instructions on what additional tests if any to add.
  • Any other feedback about this PR, broad changes you'd like to see, etc.?

To my taste this PR is done-ish. I would enjoy feedback on what stands between the PR and merging.

@BertLindeman
Copy link
Contributor

Have not seen this commit in the CI buildds

@laurensvalk
Copy link
Member

I'm wondering if the CI build is actually incorporating all the commits. @laurensvalk do you know if there is a way to check that?

You can check the current version/hash in the REPL. There should be only one definitive git history leading up to that.

One gotcha is that the hash in PR builds is the hash for GitHub's would-be merge commit, so it appears not to correspond to any branch. That history is still browseable through GitHub, though.

Or you can use the on-branch build which also runs. I usually do this to keep track of things, though only for rare cases where I suspect issues due to different compiler versions.

@jaguilar
Copy link
Contributor Author

My guess is it is not building due to the conflicts we'd need to resolve before merging. I'll rebase at some point. In the mean time, @BertLindeman, if you do want to try it, you could check out my fork and build from that.

@BertLindeman
Copy link
Contributor

My guess is it is not building due to the conflicts we'd need to resolve before merging. I'll rebase at some point. In the mean time, @BertLindeman, if you do want to try it, you could check out my fork and build from that.

Will try 😉

@jaguilar
Copy link
Contributor Author

Did some testing with the virtual hub this morning. Unfortunately, nto much progress. My virtual hub does not appear to be working even in the basics. I tried bluetooth_scan and it couldnt' see my iPhone. (Will have to try the same on the EV3 and see if I get a different result -- more on that tonight.)

jaguilar added 2 commits March 3, 2026 09:34
This implements the complete RFCOMM socket API, including listen, connect, send
and recv.

Tested via manual ping-pong testing for both listen and connect, against a
Windows desktop.
@laurensvalk
Copy link
Member

laurensvalk commented Mar 3, 2026

Thanks for submitting this! We might need a little while to digest this. Here are some initial notes to help with this.

There were many fixup! commits, so working by the assumption that these were not all independent commits, I've rebased them onto the latest master branch as a single commit. This seemed the most practical for review. If you prefer to restore this and clean up the individual commits, I'm happy to restore them. We can also split it piecewise from here as needed (e.g. I've just cherry-picked your commit to enable memory view).

Could you adapt the formatting for your changes? Many new (but not all) functions and tables are formatted across multiple lines:

pbio_error_t
pbdrv_bluetooth_rfcomm_connect(pbio_os_state_t *state, bdaddr_t bdaddr,
    int32_t timeout,
    pbdrv_bluetooth_rfcomm_conn_t *conn);

This should be fine on a single line as most things in the file. We tend to break things up only if they get really long.

We could make config a bit simpler. This isn't really needed:

#ifndef PBSYS_CONFIG_BLUETOOTH_CLASSIC_LINK_KEY_DB_SIZE
#define PBSYS_CONFIG_BLUETOOTH_CLASSIC_LINK_KEY_DB_SIZE 0
#endif

Testing #if PBSYS_CONFIG_BLUETOOTH_CLASSIC_LINK_KEY_DB_SIZE defaults to disabled.


Also on config, we have PB_LIB_UMM_MALLOC, but this introduces HAVE_UMM_MALLOC and supports both, e.g.:

    #if HAVE_UMM_MALLOC
    uint8_t *tx_buffer_data; // tx_buffer from customer. We don't own this.
    uint8_t *rx_buffer_data;
    #else
    uint8_t tx_buffer_data[RFCOMM_TX_BUFFER_SIZE];
    uint8_t rx_buffer_data[RFCOMM_RX_BUFFER_SIZE];
    #endif

Could we settle on one approach to keep things simple and more easily tested?

If the concern is Spike that didn't already have UMM, we can either enable it there too or just not enable Bluetooth classic on Spike. (It would be nice to have, but if it's taking too much RAM, maybe we shouldn't enable it. We can decide that later.)


This seems to be getting big enough to be split to a different file (not a separate driver, just for organizational purposes).

There seem to be three new namespaces. We can certainly add something to pbdrv_bluetooth_ to further group things, but this mix doesn't seem quite right.

pbdrv_bluetooth_ // existing
pbdrv_bluetooth_classic_
pbdrv_bluetooth_rfcomm
pbdrv_bluetooth_classic_rfcomm_

Bluetooth loop integration

(This is somewhat more open ended; not asking for code changes here as yet.)

As far as I can tell so far, the functions are all driven a bit differently than the way the rest of the Bluetooth loop works. Maybe this is all working as intended, but what happens if e.g. the user calls two or more pbdrv_bluetooth_rfcomm_listen or pbdrv_bluetooth_rfcomm_connect at the same time?

The current Bluetooth loop disallows certain LE operations to work in parallel, or makes things explicitly parallel or sequential where relevant. Do we need this for classic too?

There also seem to be a few runtime routines that wait for hci_get_state() == HCI_STATE_WORKING. We should probably use a different mechanism here (possibly addressed in a separate commit.)

This adds a hook pbdrv_bluetooth_rfcomm_disconnect_all to the drivers, including a no-op to the LE drivers. Do we still need this if we don't disconnect at the end of user code? I.e. could we disconnect during the BTstack shutdown routine? I haven't quite gotten to the MicroPython side of this yet. How would we handle creating a connection object and re-use an existing connection? Similar to what we do for LE connections now?

@laurensvalk laurensvalk force-pushed the ev3-bluetooth-rfcomm branch from 145625d to e66642a Compare March 3, 2026 09:22
jaguilar added 6 commits March 3, 2026 22:06
A link key db size is required now
that we don't set the default.

Also, without num_classic_connections > 0,
start_inquiry_scan is undefined.
Include btstack_config.h only when we are doing bluetooth.
@jaguilar
Copy link
Contributor Author

jaguilar commented Mar 4, 2026

Thanks for submitting this! We might need a little while to digest this. Here are some initial notes to help with this.

There were many fixup! commits, so working by the assumption that these were not all independent commits, I've rebased them onto the latest master branch as a single commit. This seemed the most practical for review. If you prefer to restore this and clean up the individual commits, I'm happy to restore them. We can also split it piecewise from here as needed (e.g. I've just cherry-picked your commit to enable memory view).

Fine by me! Would you prefer going forward that I just squash everything as I work on it? The point of the fixups was to not disturb you if you were already reviewing the existing commit, but I see it may have had the opposite effect.

Could you adapt the formatting for your changes? Many new (but not all) functions and tables are formatted across multiple lines: snip

Done. Have you guys considered adopting clang-format? I really don't care what format the code is in, as long as I don't have to do the formatting manually. But after ~15 years of not having to discuss or remember formatting it's jarring to go back to it. We could probably come up with a .clang-format that would introduce relatively few changes to the code. (And editors can be set to format only modified lines so that we don't have spurious changes everywhere.)

Also on config, we have PB_LIB_UMM_MALLOC, but this introduces HAVE_UMM_MALLOC and supports both, e.g.:

    #if HAVE_UMM_MALLOC
    uint8_t *tx_buffer_data; // tx_buffer from customer. We don't own this.
    uint8_t *rx_buffer_data;
    #else
    uint8_t tx_buffer_data[RFCOMM_TX_BUFFER_SIZE];
    uint8_t rx_buffer_data[RFCOMM_RX_BUFFER_SIZE];
    #endif

Could we settle on one approach to keep things simple and more easily tested?

Yes, I will strip classic support from primehub for now. We can add umm_malloc later. Note that this isn't done yet because it requires slightly more invasive changes.

This seems to be getting big enough to be split to a different file (not a separate driver, just for organizational purposes).

Okay, will do.

There seem to be three new namespaces. We can certainly add something to pbdrv_bluetooth_ to further group things, but this mix doesn't seem quite right.

Dropped down to just pbdrv_bluetooth_rfcomm.

Bluetooth loop integration

(This is somewhat more open ended; not asking for code changes here as yet.)

As far as I can tell so far, the functions are all driven a bit differently than the way the rest of the Bluetooth loop works. Maybe this is all working as intended, but what happens if e.g. the user calls two or more pbdrv_bluetooth_rfcomm_listen or pbdrv_bluetooth_rfcomm_connect at the same time?

For listen(), they will receive PBIO_ERROR_BUSY:

    if (pending_listen_socket) {
        // Unlike with connect, where it's plausible for multiple async contexts
        // to be connecting to different devices, it's always going to be an
        // error to listen more than once at a time.
        DEBUG_PRINT("[btc:rfcomm_listen] Already listening.\n");
        sock->err = PBIO_ERROR_BUSY;
        goto cleanup;
    }

For connect(), they will have to wait until the SDP system is free, since it can only entertain one user at a time (btstack limitation):

     // Wait until any other pending SDP query is done.
    PBIO_OS_AWAIT_UNTIL(state, !sdp_query_pending && !sdp_system_in_use);

But then after that the connect calls can proceed independently.

The current Bluetooth loop disallows certain LE operations to work in parallel, or makes things explicitly parallel or sequential where relevant. Do we need this for classic too?

Hrm, I'm not sure. What do you think should be parallel that isn't?

There also seem to be a few runtime routines that wait for hci_get_state() == HCI_STATE_WORKING. We should probably use a different mechanism here (possibly addressed in a separate commit.)

We can certainly remove those if we can guarantee these won't be called when it's not working.

This adds a hook pbdrv_bluetooth_rfcomm_disconnect_all to the drivers, including a no-op to the LE drivers. Do we still need this if we don't disconnect at the end of user code? I.e. could we disconnect during the BTstack shutdown routine? I haven't quite gotten to the MicroPython side of this yet. How would we handle creating a connection object and re-use an existing connection? Similar to what we do for LE connections now?

I don't think it would really make sense to keep the connections that back rfcomm_socket() objects open when the program has shut down. RFCOMM sockets are stream sockets, so we can't safely resume the connection without informing the other side that it has been reset.

@dlech
Copy link
Member

dlech commented Mar 4, 2026

Fine by me! Would you prefer going forward that I just squash everything as I work on it?

I've found that "Change" link GitHub makes when you force push lets us have both that the same time. The only tricky part is that if you rebase on master, the changes also include all of the unrelated commits pulled in from master. So to do it right, you should rebase without making any changes to the new commits, then force push, then change the new commits and force push again.

Have you guys considered adopting clang-format?

We adopted the linting tools from MicroPython so that the style was consistent with the upstream project. It's pretty clear though that we aren't likely to ever upstream this, so I would be fine with having something that can actually do automatic formatting of the C code if we can find some settings for it that aren't too different from what we have now. That might be hard to do though since MicroPython adds it's own style tweaks on top of what is common, like indenting #if.

@jaguilar
Copy link
Contributor Author

jaguilar commented Mar 4, 2026

clang-format 6.x has IndentPPDirectives so we can support that style as well! I don't know if it will be possible to replicate indenting directives with the corresponding C code though. I actually had the LLM code me up a clang-format optimizer so we'll see how low we can get the diff count with pbio, and you guys can decide whether it is acceptable.

Currently we have made rfcomm connections
dependent on umm_malloc, which primehub
currently does not have. As such, we need to
disable messaging since it depends on
rfcomm.
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.

4 participants