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
« 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."""
3from __future__ import annotations
5from enum import IntFlag
6from typing import TypeVar
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
22# Type variable for FlagTemplate - bound to IntFlag
23F = TypeVar("F", bound=IntFlag)
26class FlagTemplate(CodingTemplate[F]):
27 """Template for IntFlag encoding/decoding with configurable byte size.
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.
33 Pipeline Integration:
34 bytes → [extractor] → raw_int → [IDENTITY translator] → int → flag constructor
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 """
52 def __init__(self, flag_class: type[F], extractor: RawExtractor) -> None:
53 """Initialise with flag class and extractor.
55 Args:
56 flag_class: IntFlag subclass to encode/decode.
57 extractor: Raw extractor defining byte size and signedness.
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
67 @property
68 def data_size(self) -> int:
69 """Return byte size required for encoding."""
70 return self._extractor.byte_size
72 @property
73 def extractor(self) -> RawExtractor:
74 """Return extractor for pipeline access."""
75 return self._extractor
77 @property
78 def translator(self) -> ValueTranslator[int]:
79 """Get IDENTITY translator for flags (no scaling needed)."""
80 return IDENTITY
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.
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).
93 Returns:
94 Flag instance of type F.
96 Raises:
97 InsufficientDataError: If data too short for required byte size.
98 ValueRangeError: If raw value contains undefined bits and ``validate=True``.
100 """
101 if validate and len(data) < offset + self.data_size:
102 raise InsufficientDataError(self._flag_class.__name__, data[offset:], self.data_size)
104 raw_value = self._extractor.extract(data, offset)
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 )
114 return self._flag_class(raw_value)
116 def encode_value(self, value: F | int, *, validate: bool = True) -> bytearray:
117 """Encode flag instance or int to bytes.
119 Args:
120 value: Flag instance or integer value to encode.
121 validate: Whether to validate against defined flag bits (default True).
123 Returns:
124 Encoded bytes.
126 Raises:
127 ValueError: If value contains undefined bits and ``validate=True``.
129 """
130 int_value = value.value if isinstance(value, self._flag_class) else int(value)
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 )
138 return self._extractor.pack(int_value)
140 # -----------------------------------------------------------------
141 # Factory methods
142 # -----------------------------------------------------------------
144 @classmethod
145 def uint8(cls, flag_class: type[F]) -> FlagTemplate[F]:
146 """Create FlagTemplate for 1-byte unsigned flag field.
148 Args:
149 flag_class: IntFlag subclass with bit values in 0-255.
151 Returns:
152 Configured FlagTemplate instance.
154 Example::
155 >>> class Status(IntFlag):
156 ... BIT_0 = 0x01
157 ... BIT_1 = 0x02
158 >>> template = FlagTemplate.uint8(Status)
160 """
161 return cls(flag_class, UINT8)
163 @classmethod
164 def uint16(cls, flag_class: type[F]) -> FlagTemplate[F]:
165 """Create FlagTemplate for 2-byte unsigned flag field.
167 Args:
168 flag_class: IntFlag subclass with bit values in 0-65535.
170 Returns:
171 Configured FlagTemplate instance.
173 """
174 return cls(flag_class, UINT16)
176 @classmethod
177 def uint32(cls, flag_class: type[F]) -> FlagTemplate[F]:
178 """Create FlagTemplate for 4-byte unsigned flag field.
180 Args:
181 flag_class: IntFlag subclass with bit values in 0-4294967295.
183 Returns:
184 Configured FlagTemplate instance.
186 """
187 return cls(flag_class, UINT32)