Skip to content

Commit

Permalink
Merge pull request #62 from anthonytw/main
Browse files Browse the repository at this point in the history
Added 32-bit UUID support.
  • Loading branch information
dhalbert committed Apr 24, 2023
2 parents 29e3837 + 7954a9c commit 218dfc6
Show file tree
Hide file tree
Showing 2 changed files with 142 additions and 85 deletions.
65 changes: 42 additions & 23 deletions _bleio/scan_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,37 +148,56 @@ def _advertisement_fields(self) -> List[bytes]:
bytes((data_type,)) + data for data_type, data in self._data_dict.items()
)

@staticmethod
def _manufacturer_data_from_bleak(manufacturer_data: Dict[int, bytes]) -> bytes:
# The manufacturer data value is a dictionary.
# Re-concatenate it into bytes
all_mfr_data = bytearray()
for mfr_id, mfr_data in manufacturer_data.items():
all_mfr_data.extend(mfr_id.to_bytes(2, byteorder="little"))
all_mfr_data.extend(mfr_data)
return bytes(all_mfr_data)

@staticmethod
def _uuids_from_bleak(uuids: List[str]) -> bytes:
uuids16 = bytearray()
uuids32 = bytearray()
uuids128 = bytearray()
for uuid in uuids:
bleio_uuid = UUID(uuid)
# If this is a Standard UUID in 128-bit form, convert it to a 16- or 32-bit UUID.
if bleio_uuid.is_standard_uuid:
if bleio_uuid.size == 16:
uuids16.extend(bleio_uuid.uuid128[12:14])
elif bleio_uuid.size == 32:
uuids32.extend(bleio_uuid.uuid128[12:16])
else:
raise RuntimeError("Unexpected UUID size")
else:
uuids128.extend(bleio_uuid.uuid128)

fields = {}
if uuids16:
# Complete list of 16-bit UUIDs.
fields[0x03] = uuids16
if uuids32:
# Complete list of 32-bit UUIDs.
fields[0x05] = uuids32
if uuids128:
# Complete list of 128-bit UUIDs
fields[0x07] = uuids128
return fields

@staticmethod
def _data_dict_from_bleak(
device: BLEDevice, advertisement_data: AdvertisementData
) -> DataDict:
data_dict = {}
if manufacturer_data := advertisement_data.manufacturer_data:
# The manufacturer data value is a dictionary.
# Re-concatenate it into bytes
all_mfr_data = bytearray()
for mfr_id, mfr_data in manufacturer_data.items():
all_mfr_data.extend(mfr_id.to_bytes(2, byteorder="little"))
all_mfr_data.extend(mfr_data)
data_dict[0xFF] = all_mfr_data
data_dict[0xFF] = ScanEntry._manufacturer_data_from_bleak(manufacturer_data)

if uuids := advertisement_data.service_uuids:
uuids16 = bytearray()
uuids128 = bytearray()
for uuid in uuids:
bleio_uuid = UUID(uuid)
# If this is a Standard UUID in 128-bit form, convert it to a 16-bit UUID.
if bleio_uuid.is_standard_uuid:
uuids16.extend(bleio_uuid.uuid128[12:14])
else:
uuids128.extend(bleio_uuid.uuid128)

if uuids16:
# Complete list of 16-bit UUIDs.
data_dict[0x03] = uuids16
if uuids128:
# Complete list of 128-bit UUIDs
data_dict[0x07] = uuids128
data_dict.update(ScanEntry._uuids_from_bleak(uuids))

name = advertisement_data.local_name or device.name
if name and not ScanEntry._RE_IGNORABLE_NAME.fullmatch(name):
Expand Down
162 changes: 100 additions & 62 deletions _bleio/uuid_.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,76 +25,96 @@
r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", flags=re.IGNORECASE
)

_STANDARD_UUID_RE = re.compile(
_STANDARD_UUID_RE_16 = re.compile(
r"0000....-0000-1000-8000-00805f9b34fb", flags=re.IGNORECASE
)

_STANDARD_HEX_UUID_RE = re.compile(r"[0-9a-f]{1,4}", flags=re.IGNORECASE)
_STANDARD_UUID_RE_32 = re.compile(
r"........-0000-1000-8000-00805f9b34fb", flags=re.IGNORECASE
)

_STANDARD_HEX_UUID_RE = re.compile(r"[0-9a-f]{1,8}", flags=re.IGNORECASE)

_BASE_STANDARD_UUID = (
b"\xFB\x34\x9B\x5F\x80\x00\x00\x80\x00\x10\x00\x00\x00\x00\x00\x00"
)


class UUID:
@staticmethod
def standard_uuid128_from_uuid32(uuid32: int) -> bytes:
"""Return a 128-bit standard UUID from a 32-bit standard UUID."""
if not 0 <= uuid32 < 2**32:
raise ValueError("UUID integer value must be unsigned 32-bit")
return _BASE_STANDARD_UUID[:-4] + uuid32.to_bytes(4, "little")

@staticmethod
def _init_from_str(uuid: str) -> tuple[bytes, int]:
if _UUID_RE.fullmatch(uuid):
# Pick the smallest standard size.
if _STANDARD_UUID_RE_16.fullmatch(uuid):
size = 16
uuid16 = int(uuid[4:8], 16)
uuid128 = UUID.standard_uuid128_from_uuid32(uuid16)
return uuid128, size

if _STANDARD_UUID_RE_32.fullmatch(uuid):
size = 32
uuid32 = int(uuid[0:8], 16)
uuid128 = UUID.standard_uuid128_from_uuid32(uuid32)
return uuid128, size

size = 128
uuid = uuid.replace("-", "")
uuid128 = bytes(int(uuid[i : i + 2], 16) for i in range(30, -1, -2))
return uuid128, size

if _STANDARD_HEX_UUID_RE.fullmatch(uuid) and len(uuid) in (4, 8):
# Fall through and reprocess as an int.
uuid_int = int(uuid, 16)
size = len(uuid) * 4 # 4 bits per hex digit
uuid128 = UUID.standard_uuid128_from_uuid32(uuid_int)
return uuid128, size

raise ValueError(
"UUID string not 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',"
"'xxxx', or 'xxxxxxxx', but is " + uuid
)

@staticmethod
def _init_from_int(uuid: int) -> tuple[bytes, int]:
if not 0 <= uuid <= 2**32:
raise ValueError("UUID integer value must be unsigned 16- or 32-bit")
if uuid <= 2**16:
size = 16
if uuid <= 2**32:
size = 32
uuid128 = UUID.standard_uuid128_from_uuid32(uuid)
return uuid128, size

@staticmethod
def _init_from_buf(uuid: Buf) -> tuple[bytes, int]:
try:
uuid = memoryview(uuid)
except TypeError:
raise ValueError("UUID value is not str, int or byte buffer") from TypeError
if len(uuid) != 16:
raise ValueError("Byte buffer must be 16 bytes")
size = 128
uuid128 = bytes(uuid)
return uuid128, size

def __init__(self, uuid: Union[int, Buf, str]):
self.__bleak_uuid = None

if isinstance(uuid, str):
if _UUID_RE.fullmatch(uuid):
self._size = 16 if _STANDARD_UUID_RE.fullmatch(uuid) else 128
uuid = uuid.replace("-", "")
self._uuid128 = bytes(
int(uuid[i : i + 2], 16) for i in range(30, -1, -2)
)
return

if _STANDARD_HEX_UUID_RE.fullmatch(uuid):
# Fall through and reprocess as an int.
uuid = int(uuid, 16)
else:
raise ValueError(
"UUID string not 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' or 'xxxx', but is "
+ uuid
)

if isinstance(uuid, int):
if not 0 <= uuid <= 0xFFFF:
raise ValueError("UUID integer value must be 0-0xffff")
self._size = 16
self._uuid16 = uuid
# Put into "0000xxxx-0000-1000-8000-00805F9B34FB"
self._uuid128 = bytes(
(
0xFB,
0x34,
0x9B,
0x5F,
0x80,
0x00, # 00805F9B34FB
0x00,
0x80, # 8000
0x00,
0x10, # 1000
0x00,
0x00, # 0000
uuid & 0xFF,
(uuid >> 8) & 0xFF, # xxxx
0x00,
0x00,
)
) # 0000
self._uuid128, self._size = self._init_from_str(uuid)

elif isinstance(uuid, int):
self._uuid128, self._size = self._init_from_int(uuid)

else:
try:
uuid = memoryview(uuid)
except TypeError:
raise ValueError(
"UUID value is not str, int or byte buffer"
) from TypeError
if len(uuid) != 16:
raise ValueError("Byte buffer must be 16 bytes")
self._size = 128
self._uuid128 = bytes(uuid)
self._uuid128, self._size = self._init_from_buf(uuid)

@classmethod
def _from_bleak(cls, bleak_uuid: Any) -> "UUID":
Expand All @@ -112,9 +132,15 @@ def _bleak_uuid(self):

@property
def uuid16(self) -> int:
if self.size == 128:
raise ValueError("This is a 128-bit UUID")
return (self._uuid128[13] << 8) | self._uuid128[12]
if self.size > 16:
raise ValueError(f"This is a {self.size}-bit UUID")
return int.from_bytes(self._uuid128[12:14], "little")

@property
def uuid32(self) -> int:
if self.size > 32:
raise ValueError(f"This is a {self.size}-bit UUID")
return int.from_bytes(self._uuid128[12:], "little")

@property
def uuid128(self) -> bytes:
Expand All @@ -130,22 +156,30 @@ def pack_into(self, buffer, offset=0) -> None:
raise IndexError("Buffer offset too small")
if self.size == 16:
buffer[offset:byte_size] = self.uuid128[12:14]
elif self.size == 32:
buffer[offset:byte_size] = self.uuid128[12:]
else:
buffer[offset:byte_size] = self.uuid128

@property
def is_standard_uuid(self) -> bool:
"""True if this is a standard 16-bit UUID (0000xxxx-0000-1000-8000-00805F9B34FB)
"""True if this is a standard 16 or 32-bit UUID (xxxxxxxx-0000-1000-8000-00805F9B34FB)
even if it's 128-bit."""
return self.size == 16 or (
self._uuid128[0:12] == _BASE_STANDARD_UUID[0:12]
and self._uuid128[14:] == _BASE_STANDARD_UUID[14:]
return (
self.size == 16
or self.size == 32
or (
self._uuid128[0:12] == _BASE_STANDARD_UUID[0:12]
and self._uuid128[14:] == _BASE_STANDARD_UUID[14:]
)
)

def __eq__(self, other: Any) -> bool:
if isinstance(other, UUID):
if self.size == 16 and other.size == 16:
return self.uuid16 == other.uuid16
if self.size == 32 and other.size == 32:
return self.uuid32 == other.uuid32
if self.size == 128 and other.size == 128:
return self.uuid128 == other.uuid128

Expand All @@ -154,6 +188,8 @@ def __eq__(self, other: Any) -> bool:
def __hash__(self):
if self.size == 16:
return hash(self.uuid16)
if self.size == 32:
return hash(self.uuid32)
return hash(self.uuid128)

def __str__(self) -> str:
Expand All @@ -168,4 +204,6 @@ def __str__(self) -> str:
def __repr__(self) -> str:
if self.size == 16:
return f"UUID({self.uuid16:#04x})"
if self.size == 32:
return f"UUID({self.uuid32:#08x})"
return f"UUID({self!s})"

0 comments on commit 218dfc6

Please sign in to comment.