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

1"""Voice Assistant Service Control Point characteristic (0x2C33).""" 

2 

3from __future__ import annotations 

4 

5from enum import IntEnum 

6 

7import msgspec 

8 

9from ...types.gatt_enums import CharacteristicRole 

10from ..constants import SIZE_UINT8 

11from ..context import CharacteristicContext 

12from .base import BaseCharacteristic 

13from .utils import DataParser 

14 

15 

16class VoiceAssistantControlPointOpcode(IntEnum): 

17 """VAS Control Point command opcodes.""" 

18 

19 INITIALIZE_SESSION = 0x00 

20 START_SESSION = 0x01 

21 STOP_SESSION = 0x02 

22 

23 

24class VoiceAssistantControlPointResponseOpcode(IntEnum): 

25 """VAS Control Point response opcode.""" 

26 

27 RESPONSE_CODE = 0x00 

28 

29 

30class VoiceAssistantControlPointResponseCode(IntEnum): 

31 """VAS Control Point response code values.""" 

32 

33 SUCCESS = 0x01 

34 OPCODE_NOT_SUPPORTED = 0x02 

35 OPERATION_FAILED = 0x03 

36 INVALID_SESSION_STATE = 0x04 

37 

38 

39class VoiceAssistantServiceControlPointData(msgspec.Struct, frozen=True, kw_only=True): 

40 """Decoded Voice Assistant Service Control Point payload.""" 

41 

42 opcode: VoiceAssistantControlPointOpcode | VoiceAssistantControlPointResponseOpcode 

43 response_code: VoiceAssistantControlPointResponseCode | None = None 

44 

45 

46class VoiceAssistantServiceControlPointCharacteristic(BaseCharacteristic[VoiceAssistantServiceControlPointData]): 

47 """Voice Assistant Service Control Point characteristic (0x2C33). 

48 

49 org.bluetooth.characteristic.voice_assistant_service_control_point 

50 

51 Assigned Numbers defines an opcode plus optional parameters control-point 

52 structure for this characteristic. 

53 """ 

54 

55 min_length = SIZE_UINT8 

56 allow_variable_length = True 

57 _manual_role = CharacteristicRole.CONTROL 

58 

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 ) 

71 

72 opcode = VoiceAssistantControlPointOpcode(opcode_raw) 

73 self._validate_opcode_parameter(opcode, parameter, None) 

74 return VoiceAssistantServiceControlPointData(opcode=opcode) 

75 

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 

83 

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 

95 

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 

104 

105 raise ValueError("unsupported VAS control point opcode")