Coverage for src/bluetooth_sig/gatt/characteristics/csc_feature.py: 97%

30 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-30 00:10 +0000

1"""CSC Feature characteristic implementation.""" 

2 

3from __future__ import annotations 

4 

5from enum import IntFlag 

6 

7import msgspec 

8 

9from ..context import CharacteristicContext 

10from .base import BaseCharacteristic 

11from .utils import DataParser 

12 

13 

14class CSCFeatures(IntFlag): 

15 """CSC Feature flags as per Bluetooth SIG specification.""" 

16 

17 WHEEL_REVOLUTION_DATA_SUPPORTED = 0x01 

18 CRANK_REVOLUTION_DATA_SUPPORTED = 0x02 

19 MULTIPLE_SENSOR_LOCATIONS_SUPPORTED = 0x04 

20 

21 

22class CSCFeatureData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods 

23 """Parsed data from CSC Feature characteristic.""" 

24 

25 features: CSCFeatures 

26 wheel_revolution_data_supported: bool 

27 crank_revolution_data_supported: bool 

28 multiple_sensor_locations_supported: bool 

29 

30 

31class CSCFeatureCharacteristic(BaseCharacteristic): 

32 """CSC Feature characteristic (0x2A5C). 

33 

34 Used to expose the supported features of a CSC sensor. 

35 Contains a 16-bit bitmask indicating supported measurement 

36 capabilities. 

37 """ 

38 

39 def decode_value(self, data: bytearray, ctx: CharacteristicContext | None = None) -> CSCFeatureData: 

40 """Parse CSC feature data. 

41 

42 Format: 16-bit feature bitmask (little endian). 

43 

44 Args: 

45 data: Raw bytearray from BLE characteristic. 

46 ctx: Optional CharacteristicContext providing surrounding context (may be None). 

47 

48 Returns: 

49 CSCFeatureData containing parsed feature flags. 

50 

51 Raises: 

52 ValueError: If data format is invalid. 

53 

54 """ 

55 if len(data) < 2: 

56 raise ValueError("CSC Feature data must be at least 2 bytes") 

57 

58 # Parse 16-bit unsigned integer (little endian) 

59 feature_mask: int = DataParser.parse_int16(data, 0, signed=False) 

60 

61 # Parse feature flags according to specification 

62 return CSCFeatureData( 

63 features=CSCFeatures(feature_mask), 

64 wheel_revolution_data_supported=bool(feature_mask & CSCFeatures.WHEEL_REVOLUTION_DATA_SUPPORTED), 

65 crank_revolution_data_supported=bool(feature_mask & CSCFeatures.CRANK_REVOLUTION_DATA_SUPPORTED), 

66 multiple_sensor_locations_supported=bool(feature_mask & CSCFeatures.MULTIPLE_SENSOR_LOCATIONS_SUPPORTED), 

67 ) 

68 

69 def encode_value(self, data: CSCFeatureData) -> bytearray: 

70 """Encode CSC feature value back to bytes. 

71 

72 Args: 

73 data: CSCFeatureData containing CSC feature data 

74 

75 Returns: 

76 Encoded bytes representing the CSC features (uint16) 

77 

78 """ 

79 # Reconstruct the features bitmap from individual flags 

80 features_bitmap = 0 

81 if data.wheel_revolution_data_supported: 

82 features_bitmap |= CSCFeatures.WHEEL_REVOLUTION_DATA_SUPPORTED 

83 if data.crank_revolution_data_supported: 

84 features_bitmap |= CSCFeatures.CRANK_REVOLUTION_DATA_SUPPORTED 

85 if data.multiple_sensor_locations_supported: 

86 features_bitmap |= CSCFeatures.MULTIPLE_SENSOR_LOCATIONS_SUPPORTED 

87 

88 return DataParser.encode_int16(features_bitmap, signed=False)