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
4 changes: 4 additions & 0 deletions packages/pb-service/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ Notable changes to `pybricks-service`. The format is based on

## [Unreleased]

### Fixed

- Connecting over Bluetooth on macOS, which failed reading the hub's name.

## [1.0.0b1] - 2026-10-18

### Added
Expand Down
5 changes: 5 additions & 0 deletions packages/pb-service/src/pb_service/hub/ble.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ def __init__(self, client: BleakClient) -> None:
async def read_characteristic(self, uuid: str) -> bytes:
return bytes(await self._client.read_gatt_char(uuid))

async def read_name(self) -> str:
# Not a read of the Device Name characteristic, which macOS does not
# allow. bleak asks the OS for the name instead, on every platform.
return self._client.name

async def write_characteristic(
self, uuid: str, data: bytes, *, response: bool = True
) -> None:
Expand Down
4 changes: 1 addition & 3 deletions packages/pb-service/src/pb_service/hub/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,7 @@ async def _run(self, *, task_status: TaskStatus[HubInfo]) -> None:

async def _handshake(self, link: HubLink) -> HubInfo:
"""Read the hub's identity and capabilities."""
name = (await link.read_characteristic(p.DEVICE_NAME_UUID)).decode(
"utf-8", "replace"
)
name = await link.read_name()
firmware = (await link.read_characteristic(p.FIRMWARE_REVISION_UUID)).decode(
"utf-8", "replace"
)
Expand Down
8 changes: 6 additions & 2 deletions packages/pb-service/src/pb_service/hub/fake.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,8 @@ def read(self, uuid: str) -> bytes:
HubRejection: If this hub has no such characteristic.
"""

if uuid == p.DEVICE_NAME_UUID:
return self.name.encode()
# No Device Name, as on macOS, where CoreBluetooth keeps the Generic
# Access service to itself: the name has to come from elsewhere.

if uuid == p.FIRMWARE_REVISION_UUID:
return self.firmware.encode()
Expand Down Expand Up @@ -341,6 +341,10 @@ async def read_characteristic(self, uuid: str) -> bytes:
self._require_connected()
return self._hub.read(uuid)

async def read_name(self) -> str:
self._require_connected()
return self._hub.name

async def write_characteristic(
self, uuid: str, data: bytes, *, response: bool = True
) -> None:
Expand Down
5 changes: 5 additions & 0 deletions packages/pb-service/src/pb_service/hub/stream/link.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ async def read_characteristic(self, uuid: str) -> bytes:
if reply is not None and reply[0] == selector:
return reply[1]

async def read_name(self) -> str:
return (await self.read_characteristic(p.DEVICE_NAME_UUID)).decode(
"utf-8", "replace"
)

async def write_characteristic(
self, uuid: str, data: bytes, *, response: bool = True
) -> None:
Expand Down
11 changes: 11 additions & 0 deletions packages/pb-service/src/pb_service/hub/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,17 @@ async def read_characteristic(self, uuid: str) -> bytes:
"""
...

async def read_name(self) -> str:
"""Read the hub's name.

Not a characteristic read on every transport: some operating systems
keep the GATT Device Name characteristic to themselves.

Returns:
What the hub is called.
"""
...

async def write_characteristic(
self, uuid: str, data: bytes, *, response: bool = True
) -> None:
Expand Down
4 changes: 1 addition & 3 deletions packages/pb-service/src/pb_service/hub/usb/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,9 +215,7 @@ async def _probe(
async with anyio.create_task_group() as tg:
tg.start_soon(link.run, None)

name = (
await link.read_characteristic(p.DEVICE_NAME_UUID)
).decode("utf-8", "replace")
name = await link.read_name()
raw_pnp = await link.read_characteristic(p.PNP_ID_UUID)

tg.cancel_scope.cancel()
Expand Down
3 changes: 3 additions & 0 deletions packages/pb-service/tests/test_combined.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ def is_connected(self) -> bool:
async def read_characteristic(self, uuid: str) -> bytes:
return b""

async def read_name(self) -> str:
return ""

async def write_characteristic(
self, uuid: str, data: bytes, *, response: bool = True
) -> None:
Expand Down
17 changes: 17 additions & 0 deletions packages/pb-service/tests/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,23 @@ async def test_handshake_reads_identity_and_capabilities(hub: FakeHub) -> None:
connection.close("done")


async def test_handshake_names_the_hub_without_the_device_name_characteristic(
hub: FakeHub,
) -> None:
# macOS will not let the Device Name characteristic be read, and the fake
# hub does not serve it either, so reading it here fails as it did there.
# https://github.com/pybricks/support/issues/2870
transport = FakeHubTransport(hub)

with pytest.raises(HubRejection):
hub.read(p.DEVICE_NAME_UUID)

async with anyio.create_task_group() as tg:
connection, info = await _start(tg, transport, hub)
assert info.name == hub.name
connection.close("done")


async def test_old_firmware_is_refused_with_advice() -> None:
# A hub too old for the capabilities characteristic cannot be supported,
# and the message should say what to do about it.
Expand Down
Loading