Coverage for src / bluetooth_sig / gatt / characteristics / templates / composite.py: 69%
77 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"""Composite multi-field templates for time and vector data.
3Covers TimeDataTemplate, VectorTemplate, and Vector2DTemplate.
4These templates handle multi-field structures and do NOT expose extractor/translator
5since there is no single raw value to intercept.
6"""
8from __future__ import annotations
10from ....types.gatt_enums import AdjustReason, DayOfWeek
11from ...context import CharacteristicContext
12from ...exceptions import InsufficientDataError, ValueRangeError
13from ..utils import DataParser, IEEE11073Parser
14from .base import CodingTemplate
15from .data_structures import TimeData, Vector2DData, VectorData
18class TimeDataTemplate(CodingTemplate[TimeData]):
19 """Template for Bluetooth SIG time data parsing (10 bytes).
21 Used for Current Time and Time with DST characteristics.
22 Structure: Date Time (7 bytes) + Day of Week (1) + Fractions256 (1) + Adjust Reason (1)
23 """
25 LENGTH = 10
26 DAY_OF_WEEK_MAX = 7
27 FRACTIONS256_MAX = 255
28 ADJUST_REASON_MAX = 255
30 @property
31 def data_size(self) -> int:
32 """Size: 10 bytes."""
33 return self.LENGTH
35 def decode_value(
36 self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True
37 ) -> TimeData:
38 """Parse time data from bytes."""
39 if validate and len(data) < offset + self.LENGTH:
40 raise InsufficientDataError("time data", data[offset:], self.LENGTH)
42 # Parse Date Time (7 bytes)
43 year = DataParser.parse_int16(data, offset, signed=False)
44 month = data[offset + 2]
45 day = data[offset + 3]
47 date_time = None if year == 0 or month == 0 or day == 0 else IEEE11073Parser.parse_timestamp(data, offset)
49 # Parse Day of Week (1 byte)
50 day_of_week_raw = data[offset + 7]
51 if validate and day_of_week_raw > self.DAY_OF_WEEK_MAX:
52 raise ValueRangeError("day_of_week", day_of_week_raw, 0, self.DAY_OF_WEEK_MAX)
53 day_of_week = DayOfWeek(day_of_week_raw)
55 # Parse Fractions256 (1 byte)
56 fractions256 = data[offset + 8]
58 # Parse Adjust Reason (1 byte)
59 adjust_reason = AdjustReason.from_raw(data[offset + 9])
61 return TimeData(
62 date_time=date_time, day_of_week=day_of_week, fractions256=fractions256, adjust_reason=adjust_reason
63 )
65 def encode_value(self, value: TimeData, *, validate: bool = True) -> bytearray:
66 """Encode time data to bytes."""
67 result = bytearray()
69 # Encode Date Time (7 bytes)
70 if value.date_time is None:
71 result.extend(bytearray(IEEE11073Parser.TIMESTAMP_LENGTH))
72 else:
73 result.extend(IEEE11073Parser.encode_timestamp(value.date_time))
75 # Encode Day of Week (1 byte)
76 day_of_week_value = int(value.day_of_week)
77 if validate and day_of_week_value > self.DAY_OF_WEEK_MAX:
78 raise ValueRangeError("day_of_week", day_of_week_value, 0, self.DAY_OF_WEEK_MAX)
79 result.append(day_of_week_value)
81 # Encode Fractions256 (1 byte)
82 if validate and value.fractions256 > self.FRACTIONS256_MAX:
83 raise ValueRangeError("fractions256", value.fractions256, 0, self.FRACTIONS256_MAX)
84 result.append(value.fractions256)
86 # Encode Adjust Reason (1 byte)
87 if validate and int(value.adjust_reason) > self.ADJUST_REASON_MAX:
88 raise ValueRangeError("adjust_reason", int(value.adjust_reason), 0, self.ADJUST_REASON_MAX)
89 result.append(int(value.adjust_reason))
91 return result
94class VectorTemplate(CodingTemplate[VectorData]):
95 """Template for 3D vector measurements (x, y, z float32 components)."""
97 @property
98 def data_size(self) -> int:
99 """Size: 12 bytes (3 x float32)."""
100 return 12
102 def decode_value(
103 self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True
104 ) -> VectorData:
105 """Parse 3D vector data."""
106 if validate and len(data) < offset + 12:
107 raise InsufficientDataError("3D vector", data[offset:], 12)
109 x_axis = DataParser.parse_float32(data, offset)
110 y_axis = DataParser.parse_float32(data, offset + 4)
111 z_axis = DataParser.parse_float32(data, offset + 8)
113 return VectorData(x_axis=x_axis, y_axis=y_axis, z_axis=z_axis)
115 def encode_value(self, value: VectorData, *, validate: bool = True) -> bytearray:
116 """Encode 3D vector data to bytes."""
117 result = bytearray()
118 result.extend(DataParser.encode_float32(value.x_axis))
119 result.extend(DataParser.encode_float32(value.y_axis))
120 result.extend(DataParser.encode_float32(value.z_axis))
121 return result
124class Vector2DTemplate(CodingTemplate[Vector2DData]):
125 """Template for 2D vector measurements (x, y float32 components)."""
127 @property
128 def data_size(self) -> int:
129 """Size: 8 bytes (2 x float32)."""
130 return 8
132 def decode_value(
133 self, data: bytearray, offset: int = 0, ctx: CharacteristicContext | None = None, *, validate: bool = True
134 ) -> Vector2DData:
135 """Parse 2D vector data."""
136 if validate and len(data) < offset + 8:
137 raise InsufficientDataError("2D vector", data[offset:], 8)
139 x_axis = DataParser.parse_float32(data, offset)
140 y_axis = DataParser.parse_float32(data, offset + 4)
142 return Vector2DData(x_axis=x_axis, y_axis=y_axis)
144 def encode_value(self, value: Vector2DData, *, validate: bool = True) -> bytearray:
145 """Encode 2D vector data to bytes."""
146 result = bytearray()
147 result.extend(DataParser.encode_float32(value.x_axis))
148 result.extend(DataParser.encode_float32(value.y_axis))
149 return result