Coverage for src / bluetooth_sig / gatt / characteristics / udi_for_medical_devices.py: 100%
56 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-03 16:41 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-03 16:41 +0000
1"""UDI for Medical Devices characteristic (0x2BFF).
3Unique Device Identifier for medical devices per regional authority
4(e.g. US FDA). Contains label, device identifier, issuer OID, and authority OID.
6References:
7 Bluetooth SIG GATT Specification Supplement
8 org.bluetooth.characteristic.udi_for_medical_devices (GSS YAML)
9"""
11from __future__ import annotations
13from enum import IntFlag
15import msgspec
17from ..context import CharacteristicContext
18from .base import BaseCharacteristic
19from .utils import DataParser
22class UDIFlags(IntFlag):
23 """UDI for Medical Devices flags."""
25 UDI_LABEL_PRESENT = 0x01
26 UDI_DEVICE_IDENTIFIER_PRESENT = 0x02
27 UDI_ISSUER_PRESENT = 0x04
28 UDI_AUTHORITY_PRESENT = 0x08
31class UDIForMedicalDevicesData(msgspec.Struct, frozen=True, kw_only=True):
32 """Parsed data from UDI for Medical Devices characteristic.
34 Attributes:
35 flags: Presence flags for optional fields.
36 udi_label: The full UDI in human-readable form. None if absent.
37 device_identifier: The DI portion of the UDI. None if absent.
38 udi_issuer: OID of the UDI issuing organisation. None if absent.
39 udi_authority: OID of the regional UDI authority. None if absent.
41 """
43 flags: UDIFlags
44 udi_label: str | None = None
45 device_identifier: str | None = None
46 udi_issuer: str | None = None
47 udi_authority: str | None = None
50def _read_null_terminated_string(data: bytearray, offset: int) -> tuple[str, int]:
51 """Read a null-terminated UTF-8 string from data at offset.
53 Returns:
54 Tuple of (string, new_offset past the null terminator).
56 """
57 end = data.index(0x00, offset) if 0x00 in data[offset:] else len(data)
58 s = data[offset:end].decode("utf-8", errors="replace")
59 return s, end + 1 # skip past null terminator
62class UDIForMedicalDevicesCharacteristic(BaseCharacteristic[UDIForMedicalDevicesData]):
63 """UDI for Medical Devices characteristic (0x2BFF).
65 org.bluetooth.characteristic.udi_for_medical_devices
67 Contains the Unique Device Identifier assigned to a medical device.
68 """
70 min_length = 1 # At minimum, flags byte
71 allow_variable_length = True
73 def _decode_value(
74 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True
75 ) -> UDIForMedicalDevicesData:
76 """Parse UDI for Medical Devices data.
78 Format: Flags (uint8) + [UDI Label (utf8s, null-terminated)] +
79 [Device Identifier (utf8s, null-terminated)] +
80 [UDI Issuer (utf8s, null-terminated)] +
81 [UDI Authority (utf8s, null-terminated)]
82 """
83 flags = UDIFlags(DataParser.parse_int8(data, 0, signed=False))
84 offset = 1
86 udi_label: str | None = None
87 device_identifier: str | None = None
88 udi_issuer: str | None = None
89 udi_authority: str | None = None
91 if flags & UDIFlags.UDI_LABEL_PRESENT and offset < len(data):
92 udi_label, offset = _read_null_terminated_string(data, offset)
94 if flags & UDIFlags.UDI_DEVICE_IDENTIFIER_PRESENT and offset < len(data):
95 device_identifier, offset = _read_null_terminated_string(data, offset)
97 if flags & UDIFlags.UDI_ISSUER_PRESENT and offset < len(data):
98 udi_issuer, offset = _read_null_terminated_string(data, offset)
100 if flags & UDIFlags.UDI_AUTHORITY_PRESENT and offset < len(data):
101 udi_authority, offset = _read_null_terminated_string(data, offset)
103 return UDIForMedicalDevicesData(
104 flags=flags,
105 udi_label=udi_label,
106 device_identifier=device_identifier,
107 udi_issuer=udi_issuer,
108 udi_authority=udi_authority,
109 )
111 def _encode_value(self, data: UDIForMedicalDevicesData) -> bytearray:
112 """Encode UDI for Medical Devices data."""
113 result = bytearray()
114 result.extend(DataParser.encode_int8(int(data.flags), signed=False))
116 if data.udi_label is not None:
117 result.extend(data.udi_label.encode("utf-8"))
118 result.append(0x00)
120 if data.device_identifier is not None:
121 result.extend(data.device_identifier.encode("utf-8"))
122 result.append(0x00)
124 if data.udi_issuer is not None:
125 result.extend(data.udi_issuer.encode("utf-8"))
126 result.append(0x00)
128 if data.udi_authority is not None:
129 result.extend(data.udi_authority.encode("utf-8"))
130 result.append(0x00)
132 return result