Skip to content

Commit c12b503

Browse files
committed
usb: change how reading characteristics works
In the firmware, we changed how characteristics are read over USB. This adapts pybricksdev to match.
1 parent b739d0f commit c12b503

File tree

3 files changed

+145
-36
lines changed

3 files changed

+145
-36
lines changed

pybricksdev/ble/pybricks.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,18 @@ def _standard_uuid(short: int) -> str:
407407
return f"{short:08x}-0000-1000-8000-00805f9b34fb"
408408

409409

410+
def short_uuid(uuid: str) -> int:
411+
"""Gets a 16-bit from a 128-bit UUID.
412+
413+
Args:
414+
uuid: a 128-bit UUID as a string.
415+
416+
Returns:
417+
The 16-bit UUID.
418+
"""
419+
return int(uuid[4:8], 16)
420+
421+
410422
# Device Information Service: https://www.bluetooth.com/specifications/specs/device-information-service-1-1/
411423

412424
DI_SERVICE_UUID = _standard_uuid(0x180A)
@@ -415,6 +427,15 @@ def _standard_uuid(short: int) -> str:
415427
.. availability:: Since Pybricks protocol v1.0.0.
416428
"""
417429

430+
DEVICE_NAME_UUID = _standard_uuid(0x2A00)
431+
"""Standard Device Name characteristic UUID.
432+
433+
We typically don't read this directly over BLE since some OSes block reading it.
434+
Instead,we use the name from the advertising data.
435+
436+
.. availability:: Since Pybricks protocol v1.0.0.
437+
"""
438+
418439
FW_REV_UUID = _standard_uuid(0x2A26)
419440
"""Standard Firmware Revision String characteristic UUID
420441

pybricksdev/connections/pybricks.py

Lines changed: 84 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import os
88
import struct
99
from typing import Awaitable, Callable, List, Optional, TypeVar
10-
from uuid import UUID
1110

1211
import reactivex.operators as op
1312
import semver
@@ -18,14 +17,23 @@
1817
from reactivex.subject import BehaviorSubject, Subject
1918
from tqdm.auto import tqdm
2019
from tqdm.contrib.logging import logging_redirect_tqdm
21-
from usb.control import get_descriptor
2220
from usb.core import Device as USBDevice
23-
from usb.core import Endpoint, USBTimeoutError
24-
from usb.util import ENDPOINT_IN, ENDPOINT_OUT, endpoint_direction, find_descriptor
21+
from usb.core import Endpoint, Interface, USBTimeoutError
22+
from usb.util import (
23+
CTRL_IN,
24+
CTRL_RECIPIENT_INTERFACE,
25+
CTRL_TYPE_CLASS,
26+
ENDPOINT_IN,
27+
ENDPOINT_OUT,
28+
build_request_type,
29+
endpoint_direction,
30+
find_descriptor,
31+
)
2532

2633
from pybricksdev.ble.lwp3.bytecodes import HubKind
2734
from pybricksdev.ble.nus import NUS_RX_UUID, NUS_TX_UUID
2835
from pybricksdev.ble.pybricks import (
36+
DEVICE_NAME_UUID,
2937
FW_REV_UUID,
3038
PNP_ID_UUID,
3139
PYBRICKS_COMMAND_EVENT_UUID,
@@ -37,6 +45,7 @@
3745
HubCapabilityFlag,
3846
StatusFlag,
3947
UserProgramId,
48+
short_uuid,
4049
unpack_hub_capabilities,
4150
unpack_pnp_id,
4251
)
@@ -45,7 +54,9 @@
4554
from pybricksdev.tools import chunk
4655
from pybricksdev.tools.checksum import xor_bytes
4756
from pybricksdev.usb.pybricks import (
57+
PYBRICKS_USB_INTERFACE_CLASS_REQUEST_MAX_SIZE,
4858
PybricksUsbInEpMessageType,
59+
PybricksUsbInterfaceClassRequest,
4960
PybricksUsbOutEpMessageType,
5061
)
5162

@@ -786,6 +797,40 @@ async def start_notify(self, uuid: str, callback: Callable) -> None:
786797
return await self._client.start_notify(uuid, callback)
787798

788799

800+
def get_interface_class_data(
801+
dev: USBDevice,
802+
interface: Interface,
803+
desc_size: int,
804+
request: int = 0,
805+
value: int = 0,
806+
) -> bytes:
807+
"""
808+
Get a vendor-specific descriptor.
809+
810+
usb.util doesn't have a method like this, so we have to do it ourselves.
811+
812+
Args:
813+
dev: The USB device.
814+
vendor_code: The vendor code from the BOS descriptor.
815+
value: The wValue field of the request.
816+
index: The wIndex field of the request.
817+
"""
818+
819+
bmRequestType = build_request_type(
820+
CTRL_IN, CTRL_TYPE_CLASS, CTRL_RECIPIENT_INTERFACE
821+
)
822+
823+
ret = dev.ctrl_transfer(
824+
bmRequestType=bmRequestType,
825+
bRequest=request,
826+
wValue=value,
827+
wIndex=interface.index,
828+
data_or_wLength=desc_size,
829+
)
830+
831+
return bytes(ret)
832+
833+
789834
class PybricksHubUSB(PybricksHub):
790835
_device: USBDevice
791836
_ep_in: Endpoint
@@ -820,46 +865,49 @@ async def _client_connect(self) -> bool:
820865
# There is 1 byte overhead for PybricksUsbMessageType
821866
self._max_write_size = self._ep_out.wMaxPacketSize - 1
822867

823-
# Get length of BOS descriptor
824-
bos_descriptor = get_descriptor(self._device, 5, 0x0F, 0)
825-
(ofst, _, bos_len, _) = struct.unpack("<BBHB", bos_descriptor)
826-
827-
# Get full BOS descriptor
828-
bos_descriptor = get_descriptor(self._device, bos_len, 0x0F, 0)
868+
# This is the equivalent of reading GATT characteristics for BLE connections.
829869

830-
while ofst < bos_len:
831-
(size, desc_type, cap_type) = struct.unpack_from(
832-
"<BBB", bos_descriptor, offset=ofst
870+
def read_data(req: PybricksUsbInterfaceClassRequest, value: int) -> bytes:
871+
return get_interface_class_data(
872+
self._device,
873+
intf,
874+
PYBRICKS_USB_INTERFACE_CLASS_REQUEST_MAX_SIZE,
875+
req,
876+
value,
833877
)
834878

835-
if desc_type != 0x10:
836-
logger.error("Expected Device Capability descriptor")
837-
exit(1)
879+
hub_name_desc = read_data(
880+
PybricksUsbInterfaceClassRequest.GATT_CHARACTERISTIC,
881+
short_uuid(DEVICE_NAME_UUID),
882+
)
838883

839-
# Look for platform descriptors
840-
if cap_type == 0x05:
841-
uuid_bytes = bos_descriptor[ofst + 4 : ofst + 4 + 16]
842-
uuid_str = str(UUID(bytes_le=bytes(uuid_bytes)))
884+
self._hub_name = str(hub_name_desc, "utf-8")
843885

844-
if uuid_str == FW_REV_UUID:
845-
fw_version = bytearray(bos_descriptor[ofst + 20 : ofst + size])
846-
self.fw_version = Version(fw_version.decode())
886+
fw_version_desc = read_data(
887+
PybricksUsbInterfaceClassRequest.GATT_CHARACTERISTIC,
888+
short_uuid(FW_REV_UUID),
889+
)
847890

848-
elif uuid_str == SW_REV_UUID:
849-
self._protocol_version = bytearray(
850-
bos_descriptor[ofst + 20 : ofst + size]
851-
)
891+
self.fw_version = Version(str(fw_version_desc, "utf-8"))
852892

853-
elif uuid_str == PYBRICKS_HUB_CAPABILITIES_UUID:
854-
caps = bytearray(bos_descriptor[ofst + 20 : ofst + size])
855-
(
856-
_,
857-
self._capability_flags,
858-
self._max_user_program_size,
859-
self._num_of_slots,
860-
) = unpack_hub_capabilities(caps)
893+
sw_version_desc = read_data(
894+
PybricksUsbInterfaceClassRequest.GATT_CHARACTERISTIC,
895+
short_uuid(SW_REV_UUID),
896+
)
897+
898+
self._protocol_version = str(sw_version_desc, "utf-8")
899+
900+
hub_caps_desc = read_data(
901+
PybricksUsbInterfaceClassRequest.PYBRICKS_CHARACTERISTIC,
902+
short_uuid(PYBRICKS_HUB_CAPABILITIES_UUID),
903+
)
861904

862-
ofst += size
905+
(
906+
_,
907+
self._capability_flags,
908+
self._max_user_program_size,
909+
self._num_of_slots,
910+
) = unpack_hub_capabilities(hub_caps_desc)
863911

864912
self._monitor_task = asyncio.create_task(self._monitor_usb())
865913

pybricksdev/usb/pybricks.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,51 @@
33

44
"""
55
Pybricks-specific USB protocol.
6+
7+
This is generally a wrapper around the Pybricks BLE protocol adapted for USB.
68
"""
79

810
from enum import IntEnum
911

1012

13+
class PybricksUsbInterfaceClassRequest(IntEnum):
14+
"""
15+
Request type for the Pybricks USB interface class.
16+
17+
This is passed as bRequest in the USB control transfer where wIndex is the
18+
interface number of the Pybricks USB interface and bmRequestType has type
19+
of Class (1) and Recipient of Interface (1).
20+
"""
21+
22+
GATT_CHARACTERISTIC = 1
23+
"""
24+
Analogous to standard BLE GATT characteristics.
25+
26+
bValue is the 16-bit UUID of the characteristic.
27+
"""
28+
29+
PYBRICKS_CHARACTERISTIC = 2
30+
"""
31+
Analogous to custom BLE characteristics in the Pybricks Service.
32+
33+
bValue is the 16-bit UUID of the characteristic (3rd and 4th bytes of the 128-bit UUID).
34+
"""
35+
36+
37+
PYBRICKS_USB_INTERFACE_CLASS_REQUEST_MAX_SIZE = 20
38+
"""
39+
The maximum size of data that can be sent or received in a single control transfer
40+
using the PybricksUsbInterfaceClassRequest interface class requests.
41+
42+
This limit comes from the smallest MTU of BLE (23 bytes) minus the 3-byte ATT header.
43+
44+
The Pybricks interface just uses data and doesn't use USB-style descriptors that
45+
include the length. We can get away with this by limiting the size of the data
46+
for each characteristic to be less than or equal to this value. Then, we can
47+
always pass this as the wLength when reading.
48+
"""
49+
50+
1151
class PybricksUsbInEpMessageType(IntEnum):
1252
RESPONSE = 1
1353
"""

0 commit comments

Comments
 (0)