Coverage for src/bluetooth_sig/gatt/characteristics/voice_assistant_service_control_point.py: 98%
54 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-28 01:26 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-28 01:26 +0000
1"""Voice Assistant Service Control Point characteristic (0x2C33)."""
3from __future__ import annotations
5from enum import IntEnum
7import msgspec
9from ...types.gatt_enums import CharacteristicRole
10from ..constants import SIZE_UINT8
11from ..context import CharacteristicContext
12from .base import BaseCharacteristic
13from .utils import DataParser
16class VoiceAssistantControlPointOpcode(IntEnum):
17 """VAS Control Point command opcodes."""
19 INITIALIZE_SESSION = 0x00
20 START_SESSION = 0x01
21 STOP_SESSION = 0x02
24class VoiceAssistantControlPointResponseOpcode(IntEnum):
25 """VAS Control Point response opcode."""
27 RESPONSE_CODE = 0x00
30class VoiceAssistantControlPointResponseCode(IntEnum):
31 """VAS Control Point response code values."""
33 SUCCESS = 0x01
34 OPCODE_NOT_SUPPORTED = 0x02
35 OPERATION_FAILED = 0x03
36 INVALID_SESSION_STATE = 0x04
39class VoiceAssistantServiceControlPointData(msgspec.Struct, frozen=True, kw_only=True):
40 """Decoded Voice Assistant Service Control Point payload."""
42 opcode: VoiceAssistantControlPointOpcode | VoiceAssistantControlPointResponseOpcode
43 response_code: VoiceAssistantControlPointResponseCode | None = None
46class VoiceAssistantServiceControlPointCharacteristic(BaseCharacteristic[VoiceAssistantServiceControlPointData]):
47 """Voice Assistant Service Control Point characteristic (0x2C33).
49 org.bluetooth.characteristic.voice_assistant_service_control_point
51 Assigned Numbers defines an opcode plus optional parameters control-point
52 structure for this characteristic.
53 """
55 min_length = SIZE_UINT8
56 allow_variable_length = True
57 _manual_role = CharacteristicRole.CONTROL
59 def _decode_value(
60 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True
61 ) -> VoiceAssistantServiceControlPointData:
62 opcode_raw = DataParser.parse_int8(data, 0, signed=False)
63 parameter = bytes(data[SIZE_UINT8:])
64 if opcode_raw == VoiceAssistantControlPointResponseOpcode.RESPONSE_CODE and parameter:
65 if len(parameter) != SIZE_UINT8:
66 raise ValueError("response code opcode requires one response-code byte")
67 return VoiceAssistantServiceControlPointData(
68 opcode=VoiceAssistantControlPointResponseOpcode.RESPONSE_CODE,
69 response_code=VoiceAssistantControlPointResponseCode(parameter[0]),
70 )
72 opcode = VoiceAssistantControlPointOpcode(opcode_raw)
73 self._validate_opcode_parameter(opcode, parameter, None)
74 return VoiceAssistantServiceControlPointData(opcode=opcode)
76 def _encode_value(self, data: VoiceAssistantServiceControlPointData) -> bytearray:
77 self._validate_opcode_parameter(data.opcode, b"", data.response_code)
78 result = bytearray()
79 result.extend(DataParser.encode_int8(int(data.opcode), signed=False))
80 if data.response_code is not None:
81 result.extend(DataParser.encode_int8(int(data.response_code), signed=False))
82 return result
84 @staticmethod
85 def _validate_opcode_parameter(
86 opcode: VoiceAssistantControlPointOpcode | VoiceAssistantControlPointResponseOpcode,
87 parameter: bytes,
88 response_code: VoiceAssistantControlPointResponseCode | None,
89 ) -> None:
90 """Validate the opcode and parameter layout from VAS Tables 3.4 and 3.5."""
91 if opcode == VoiceAssistantControlPointResponseOpcode.RESPONSE_CODE:
92 if parameter:
93 raise ValueError("response code is represented by response_code, not raw parameter bytes")
94 return
96 if opcode in {
97 VoiceAssistantControlPointOpcode.INITIALIZE_SESSION,
98 VoiceAssistantControlPointOpcode.START_SESSION,
99 VoiceAssistantControlPointOpcode.STOP_SESSION,
100 }:
101 if parameter or response_code is not None:
102 raise ValueError("VAS command opcodes do not include parameters")
103 return
105 raise ValueError("unsupported VAS control point opcode")