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

1"""UDI for Medical Devices characteristic (0x2BFF). 

2 

3Unique Device Identifier for medical devices per regional authority 

4(e.g. US FDA). Contains label, device identifier, issuer OID, and authority OID. 

5 

6References: 

7 Bluetooth SIG GATT Specification Supplement 

8 org.bluetooth.characteristic.udi_for_medical_devices (GSS YAML) 

9""" 

10 

11from __future__ import annotations 

12 

13from enum import IntFlag 

14 

15import msgspec 

16 

17from ..context import CharacteristicContext 

18from .base import BaseCharacteristic 

19from .utils import DataParser 

20 

21 

22class UDIFlags(IntFlag): 

23 """UDI for Medical Devices flags.""" 

24 

25 UDI_LABEL_PRESENT = 0x01 

26 UDI_DEVICE_IDENTIFIER_PRESENT = 0x02 

27 UDI_ISSUER_PRESENT = 0x04 

28 UDI_AUTHORITY_PRESENT = 0x08 

29 

30 

31class UDIForMedicalDevicesData(msgspec.Struct, frozen=True, kw_only=True): 

32 """Parsed data from UDI for Medical Devices characteristic. 

33 

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. 

40 

41 """ 

42 

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 

48 

49 

50def _read_null_terminated_string(data: bytearray, offset: int) -> tuple[str, int]: 

51 """Read a null-terminated UTF-8 string from data at offset. 

52 

53 Returns: 

54 Tuple of (string, new_offset past the null terminator). 

55 

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 

60 

61 

62class UDIForMedicalDevicesCharacteristic(BaseCharacteristic[UDIForMedicalDevicesData]): 

63 """UDI for Medical Devices characteristic (0x2BFF). 

64 

65 org.bluetooth.characteristic.udi_for_medical_devices 

66 

67 Contains the Unique Device Identifier assigned to a medical device. 

68 """ 

69 

70 min_length = 1 # At minimum, flags byte 

71 allow_variable_length = True 

72 

73 def _decode_value( 

74 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True 

75 ) -> UDIForMedicalDevicesData: 

76 """Parse UDI for Medical Devices data. 

77 

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 

85 

86 udi_label: str | None = None 

87 device_identifier: str | None = None 

88 udi_issuer: str | None = None 

89 udi_authority: str | None = None 

90 

91 if flags & UDIFlags.UDI_LABEL_PRESENT and offset < len(data): 

92 udi_label, offset = _read_null_terminated_string(data, offset) 

93 

94 if flags & UDIFlags.UDI_DEVICE_IDENTIFIER_PRESENT and offset < len(data): 

95 device_identifier, offset = _read_null_terminated_string(data, offset) 

96 

97 if flags & UDIFlags.UDI_ISSUER_PRESENT and offset < len(data): 

98 udi_issuer, offset = _read_null_terminated_string(data, offset) 

99 

100 if flags & UDIFlags.UDI_AUTHORITY_PRESENT and offset < len(data): 

101 udi_authority, offset = _read_null_terminated_string(data, offset) 

102 

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 ) 

110 

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)) 

115 

116 if data.udi_label is not None: 

117 result.extend(data.udi_label.encode("utf-8")) 

118 result.append(0x00) 

119 

120 if data.device_identifier is not None: 

121 result.extend(data.device_identifier.encode("utf-8")) 

122 result.append(0x00) 

123 

124 if data.udi_issuer is not None: 

125 result.extend(data.udi_issuer.encode("utf-8")) 

126 result.append(0x00) 

127 

128 if data.udi_authority is not None: 

129 result.extend(data.udi_authority.encode("utf-8")) 

130 result.append(0x00) 

131 

132 return result