Coverage for src / bluetooth_sig / gatt / characteristics / templates / flag.py: 87%

46 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-18 11:17 +0000

1"""Flag template for IntFlag encoding/decoding with configurable byte size.""" 

2 

3from __future__ import annotations 

4 

5from enum import IntFlag 

6from typing import TypeVar 

7 

8from ...context import CharacteristicContext 

9from ...exceptions import InsufficientDataError, ValueRangeError 

10from ..utils.extractors import ( 

11 UINT8, 

12 UINT16, 

13 UINT32, 

14 RawExtractor, 

15) 

16from ..utils.translators import ( 

17 IDENTITY, 

18 ValueTranslator, 

19) 

20from .base import CodingTemplate 

21 

22# Type variable for FlagTemplate - bound to IntFlag 

23F = TypeVar("F", bound=IntFlag) 

24 

25 

26class FlagTemplate(CodingTemplate[F]): 

27 """Template for IntFlag encoding/decoding with configurable byte size. 

28 

29 Maps raw integer bytes to Python IntFlag instances through extraction and 

30 validation. Unlike EnumTemplate (which expects exact enum membership), 

31 FlagTemplate accepts any bitwise OR combination of the defined flag members. 

32 

33 Pipeline Integration: 

34 bytes → [extractor] → raw_int → [IDENTITY translator] → int → flag constructor 

35 

36 Examples: 

37 >>> class ContactStatus(IntFlag): 

38 ... CONTACT_0 = 0x01 

39 ... CONTACT_1 = 0x02 

40 ... CONTACT_2 = 0x04 

41 >>> 

42 >>> template = FlagTemplate.uint8(ContactStatus) 

43 >>> 

44 >>> # Decode from bytes — any combination is valid 

45 >>> flags = template.decode_value(bytearray([0x05])) 

46 >>> # ContactStatus.CONTACT_0 | ContactStatus.CONTACT_2 

47 >>> 

48 >>> # Encode flags to bytes 

49 >>> data = template.encode_value(ContactStatus.CONTACT_0 | ContactStatus.CONTACT_2) # bytearray([0x05]) 

50 """ 

51 

52 def __init__(self, flag_class: type[F], extractor: RawExtractor) -> None: 

53 """Initialise with flag class and extractor. 

54 

55 Args: 

56 flag_class: IntFlag subclass to encode/decode. 

57 extractor: Raw extractor defining byte size and signedness. 

58 

59 """ 

60 self._flag_class = flag_class 

61 self._extractor = extractor 

62 # Pre-compute the bitmask of all defined members for validation. 

63 self._valid_mask: int = 0 

64 for member in flag_class: 

65 self._valid_mask |= member.value 

66 

67 @property 

68 def data_size(self) -> int: 

69 """Return byte size required for encoding.""" 

70 return self._extractor.byte_size 

71 

72 @property 

73 def extractor(self) -> RawExtractor: 

74 """Return extractor for pipeline access.""" 

75 return self._extractor 

76 

77 @property 

78 def translator(self) -> ValueTranslator[int]: 

79 """Get IDENTITY translator for flags (no scaling needed).""" 

80 return IDENTITY 

81 

82 def decode_value( 

83 self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True 

84 ) -> F: 

85 """Decode bytes to flag instance. 

86 

87 Args: 

88 data: Raw bytes from BLE characteristic. 

89 offset: Starting offset in data buffer. 

90 ctx: Optional context for parsing. 

91 validate: Whether to validate against defined flag bits (default True). 

92 

93 Returns: 

94 Flag instance of type F. 

95 

96 Raises: 

97 InsufficientDataError: If data too short for required byte size. 

98 ValueRangeError: If raw value contains undefined bits and ``validate=True``. 

99 

100 """ 

101 if validate and len(data) < offset + self.data_size: 

102 raise InsufficientDataError(self._flag_class.__name__, data[offset:], self.data_size) 

103 

104 raw_value = self._extractor.extract(data, offset) 

105 

106 if validate and (raw_value & ~self._valid_mask): 

107 raise ValueRangeError( 

108 self._flag_class.__name__, 

109 raw_value, 

110 0, 

111 self._valid_mask, 

112 ) 

113 

114 return self._flag_class(raw_value) 

115 

116 def encode_value(self, value: F | int, *, validate: bool = True) -> bytearray: 

117 """Encode flag instance or int to bytes. 

118 

119 Args: 

120 value: Flag instance or integer value to encode. 

121 validate: Whether to validate against defined flag bits (default True). 

122 

123 Returns: 

124 Encoded bytes. 

125 

126 Raises: 

127 ValueError: If value contains undefined bits and ``validate=True``. 

128 

129 """ 

130 int_value = value.value if isinstance(value, self._flag_class) else int(value) 

131 

132 if validate and (int_value & ~self._valid_mask): 

133 raise ValueError( 

134 f"{self._flag_class.__name__} value 0x{int_value:02X} contains " 

135 f"undefined bits (valid mask: 0x{self._valid_mask:02X})" 

136 ) 

137 

138 return self._extractor.pack(int_value) 

139 

140 # ----------------------------------------------------------------- 

141 # Factory methods 

142 # ----------------------------------------------------------------- 

143 

144 @classmethod 

145 def uint8(cls, flag_class: type[F]) -> FlagTemplate[F]: 

146 """Create FlagTemplate for 1-byte unsigned flag field. 

147 

148 Args: 

149 flag_class: IntFlag subclass with bit values in 0-255. 

150 

151 Returns: 

152 Configured FlagTemplate instance. 

153 

154 Example:: 

155 >>> class Status(IntFlag): 

156 ... BIT_0 = 0x01 

157 ... BIT_1 = 0x02 

158 >>> template = FlagTemplate.uint8(Status) 

159 

160 """ 

161 return cls(flag_class, UINT8) 

162 

163 @classmethod 

164 def uint16(cls, flag_class: type[F]) -> FlagTemplate[F]: 

165 """Create FlagTemplate for 2-byte unsigned flag field. 

166 

167 Args: 

168 flag_class: IntFlag subclass with bit values in 0-65535. 

169 

170 Returns: 

171 Configured FlagTemplate instance. 

172 

173 """ 

174 return cls(flag_class, UINT16) 

175 

176 @classmethod 

177 def uint32(cls, flag_class: type[F]) -> FlagTemplate[F]: 

178 """Create FlagTemplate for 4-byte unsigned flag field. 

179 

180 Args: 

181 flag_class: IntFlag subclass with bit values in 0-4294967295. 

182 

183 Returns: 

184 Configured FlagTemplate instance. 

185 

186 """ 

187 return cls(flag_class, UINT32)