Coverage for src / bluetooth_sig / gatt / descriptors / cccd.py: 82%
44 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 20:14 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-11 20:14 +0000
1"""Client Characteristic Configuration Descriptor implementation."""
3from __future__ import annotations
5from enum import IntFlag
7import msgspec
9from ..characteristics.utils import DataParser
10from .base import BaseDescriptor
13class CCCDFlags(IntFlag):
14 """CCCD (Client Characteristic Configuration Descriptor) flags."""
16 NOTIFICATIONS_ENABLED = 0x0001
17 INDICATIONS_ENABLED = 0x0002
20class CCCDData(msgspec.Struct, frozen=True, kw_only=True):
21 """CCCD (Client Characteristic Configuration Descriptor) data."""
23 notifications_enabled: bool
24 indications_enabled: bool
27class CCCDDescriptor(BaseDescriptor):
28 """Client Characteristic Configuration Descriptor (0x2902).
30 Controls notification and indication settings for a characteristic.
31 Critical for enabling BLE notifications and indications.
32 """
34 _descriptor_name = "Client Characteristic Configuration"
35 _writable = True # CCCD is always writable
37 def _has_structured_data(self) -> bool:
38 return True
40 def _get_data_format(self) -> str:
41 return "uint16"
43 def _parse_descriptor_value(self, data: bytes) -> CCCDData:
44 """Parse CCCD value into notification/indication flags.
46 Args:
47 data: Raw bytes (should be 2 bytes for uint16)
49 Returns:
50 CCCDData with notification/indication flags
52 Raises:
53 ValueError: If data is not exactly 2 bytes
54 """
55 if len(data) != 2:
56 raise ValueError(f"CCCD data must be exactly 2 bytes, got {len(data)}")
58 # Parse as little-endian uint16
59 value = DataParser.parse_int16(data, endian="little")
61 return CCCDData(
62 notifications_enabled=bool(value & CCCDFlags.NOTIFICATIONS_ENABLED),
63 indications_enabled=bool(value & CCCDFlags.INDICATIONS_ENABLED),
64 )
66 @staticmethod
67 def create_enable_notifications_value() -> bytes:
68 """Create value to enable notifications."""
69 return CCCDFlags.NOTIFICATIONS_ENABLED.to_bytes(2, "little")
71 @staticmethod
72 def create_enable_indications_value() -> bytes:
73 """Create value to enable indications."""
74 return CCCDFlags.INDICATIONS_ENABLED.to_bytes(2, "little")
76 @staticmethod
77 def create_enable_both_value() -> bytes:
78 """Create value to enable both notifications and indications."""
79 return (CCCDFlags.NOTIFICATIONS_ENABLED | CCCDFlags.INDICATIONS_ENABLED).to_bytes(2, "little")
81 @staticmethod
82 def create_disable_value() -> bytes:
83 """Create value to disable notifications/indications."""
84 return (0).to_bytes(2, "little")
86 def is_notifications_enabled(self, data: bytes) -> bool:
87 """Check if notifications are enabled."""
88 parsed = self._parse_descriptor_value(data)
89 return parsed.notifications_enabled
91 def is_indications_enabled(self, data: bytes) -> bool:
92 """Check if indications are enabled."""
93 parsed = self._parse_descriptor_value(data)
94 return parsed.indications_enabled
96 def is_any_enabled(self, data: bytes) -> bool:
97 """Check if either notifications or indications are enabled."""
98 parsed = self._parse_descriptor_value(data)
99 return parsed.notifications_enabled or parsed.indications_enabled