You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
EV3 and NXT hubs over Bluetooth Classic (RFCOMM) - #6
Adds an RFCOMM transport to pb-service, so EV3 and NXT hubs can be reached over Bluetooth Classic on Linux, macOS and Windows through aio-rfcomm 1.0.
Shared byte stream: COBS framing, the wire envelope and the link move from hub/usb to hub/stream. USB and RFCOMM share them. No behavior change for USB.
The transport (hub/rfcomm): discovery is the set of paired devices. An EV3 matches on its Pybricks service class. An NXT matches on plain Serial Port plus the "(MINDSTORMS NXT)" name suffix.
Wiring: the service opens Bluetooth once, under the "Pybricks" name. PB_HUB_BACKEND=rfcomm selects RFCOMM alone. RFCOMM ranks after BLE when a hub is reachable both ways. An unreachable hub reports HUB_NOT_CONNECTED.
CLI:rfcomm is a transport to ask for. RFCOMM rows in pb scan say "paired" instead of an empty signal meter. pb status says "Bluetooth Classic". When nothing is found over RFCOMM, it asks about pairing.
Tested against a Pybricks EV3 on firmware 4.1.0:
the hardware tests pass over RFCOMM on macOS 26.6.2 (7 of 7, with aio-rfcomm 1.0.0 from PyPI);
connect, status, run and disconnect work on Windows 10.0.26200.9711;
earlier, on Linux with BlueZ 5.85, the same checks passed, along with switching the hub off mid-session and opening it while off.
The EV3 and NXT speak over an RFCOMM channel exactly what the other hubs speak
over USB: the same COBS variant, the same envelope from Pybricks Profile v1.6.0,
and the same rule of one request at a time. So everything that was about the
byte stream rather than about serial ports moves to hub/stream: cobs and wire
as they were, the link that was _UsbLink as StreamLink, and SerialPort as
BytePipe -- the two methods of a stream the link reads, which an RFCOMM channel
can offer as readily as a serial port can.
Nothing changes in behaviour. The one visible difference is wording: a
characteristic with no equivalent on a stream says so, rather than naming USB.
The EV3 and NXT have no BLE, so they are reached over RFCOMM, through
aio-rfcomm, which gives one asyncio API over BlueZ, IOBluetooth and WinRT. What
crosses the channel is the framed stream USB already carries, so the transport
is mostly a matter of finding hubs and handing the channel to StreamLink.
Finding them is not a scan and cannot be one. Bluetooth Classic pairs in the
operating system, so what is listed is the paired set: a hub appears as soon
as the computer knows it and stays listed whether it is switched on or not.
Only opening it says which, so a hub that is off arrives as ConnectionError
from open() -- the routine way to fail here, not an edge case.
A hub is recognised by its service class. The EV3 has its own, from the 0x01xx
block of the Pybricks UUIDs, which is not GATT attributes. The NXT publishes
only plain Serial Port, which every paired headset offers too, so one of those
counts only when its name ends in the "(MINDSTORMS NXT)" the firmware appends.
The same suffix gives the type for the scan's own column, and is taken off the
name shown.
Bluetooth being off, or this program not being allowed it, is reported as the
transport's one unavailable entry rather than raised, so the other transports
still answer. Discovery follows Bluetooth's availability as well as polling the
paired set every 2 s, so that entry appears and clears as soon as the switch
does.
aio-rfcomm comes from PyPI. It adds rubicon-objc on macOS and the WinRT RFCOMM
projection on Windows. On Linux it needs only dbus-fast, which bleak already
brings.
The service now opens Bluetooth Classic itself and hands it to the RFCOMM
transport, next to USB and BLE. aio-rfcomm allows it open once per process,
and everything opened through it stops when its block ends, so it is opened
around the hub manager and outlives every hub. PB_HUB_BACKEND=rfcomm uses it
alone, and is then the one case where failing to open it is an error; with
every transport in use, RFCOMM is left out with a warning and the rest work.
On macOS the service asks for Bluetooth under the name "Pybricks". A service
started from a command line has no application to speak for it, so without a
name it is refused rather than asked, and macOS remembers the answer under the
name, so it must not change.
RFCOMM ranks after BLE when a hub is reachable more than one way, because its
listing includes hubs that are switched off, where hearing a hub advertise
says it is there. In practice they rarely compete: a hub speaks one radio or
the other.
Connecting to a hub that cannot be reached now fails as HUB_NOT_CONNECTED with
the transport's reason in the log, rather than as an internal error. It was
reachable on USB by unplugging between the scan and the connect, and on RFCOMM
it is the ordinary result for a paired hub that is switched off.
pb scan and pb connect pass --transport through to the service unchecked, so
--transport rfcomm worked as soon as the service knew it. What the CLI says
around it had not caught up:
- The help named two transports, and now names three.
- A hub found over Bluetooth Classic had an empty signal meter, which reads
as a hub at the edge of range. Nothing was heard at all: the listing is the
paired set, and the hub may be switched off. Its row now says "paired".
- pb status showed the service's own word, "rfcomm". It is now "Bluetooth
Classic", Bluetooth's name for the older radio, which keeps it apart from
the BLE hubs shown as "Bluetooth".
- Finding nothing over RFCOMM means nothing is paired, so pb scan and
pb connect --transport rfcomm now ask about pairing rather than suggesting
the hub be switched on or its button pressed.
… open
With Bluetooth switched off or not allowed, RFCOMM cannot read the paired
set, and it reported the adapter as an unavailable device. Finding nothing
then raised SERIAL_PORT_UNAVAILABLE, and the CLI told the user to join
dialout or unplug a hub. An unavailable device now says whether it is a
port or the adapter. An adapter gives BLUETOOTH_UNAVAILABLE with the
system's own words, and the CLI's advice covers Bluetooth Classic too.
…error
Both stream transports read the link in a task group beside the caller's
block, and a group wraps whatever leaves it. A channel or cable that
dropped before the handshake finished came out as an ExceptionGroup,
which connecting did not catch and reported as INTERNAL_ERROR rather than
HUB_NOT_CONNECTED. StreamLink.running now owns that task group, and a
failure in it comes out as itself.
The service opened Bluetooth before it started listening, and opening it
does not return until the system says whether Bluetooth is usable. On
CI's macOS runners that never happened, so the service never started and
every end-to-end test that runs one timed out, USB and BLE included. A
Mac whose user has not yet answered the permission prompt would do the
same.
It is now opened the first time a scan or a connection over RFCOMM asks,
in a task of the transport's own scope, as bleak does for BLE. A service
used only over USB never starts aio-rfcomm's helper. If opening fails,
RFCOMM reports Bluetooth as unusable and the other transports carry on.
strip() removes whitespace belonging to the hub's actual name, not just the firmware separator. For example, " Bumblebee (MINDSTORMS EV3)" becomes "Bumblebee", while the handshake preserves " Bumblebee ". RFCOMM discovery compares names by exact equality, so connecting by the original name fails. Remove only the single separator space before the suffix, and add a regression case with leading and trailing whitespace.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds an RFCOMM transport to pb-service, so EV3 and NXT hubs can be reached over Bluetooth Classic on Linux, macOS and Windows through aio-rfcomm 1.0.
hub/usbtohub/stream. USB and RFCOMM share them. No behavior change for USB.hub/rfcomm): discovery is the set of paired devices. An EV3 matches on its Pybricks service class. An NXT matches on plain Serial Port plus the "(MINDSTORMS NXT)" name suffix.PB_HUB_BACKEND=rfcommselects RFCOMM alone. RFCOMM ranks after BLE when a hub is reachable both ways. An unreachable hub reportsHUB_NOT_CONNECTED.rfcommis a transport to ask for. RFCOMM rows inpb scansay "paired" instead of an empty signal meter.pb statussays "Bluetooth Classic". When nothing is found over RFCOMM, it asks about pairing.Tested against a Pybricks EV3 on firmware 4.1.0:
The changelogs are not updated here.