Coverage for src / bluetooth_sig / gatt / characteristics / rsc_feature.py: 100%

38 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-18 11:17 +0000

1"""RSC 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 RSCFeatures(IntFlag): 

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

16 

17 INSTANTANEOUS_STRIDE_LENGTH_SUPPORTED = 0x01 

18 TOTAL_DISTANCE_SUPPORTED = 0x02 

19 WALKING_OR_RUNNING_STATUS_SUPPORTED = 0x04 

20 CALIBRATION_PROCEDURE_SUPPORTED = 0x08 

21 MULTIPLE_SENSOR_LOCATIONS_SUPPORTED = 0x10 

22 

23 

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

25 """Parsed data from RSC Feature characteristic.""" 

26 

27 features: RSCFeatures 

28 instantaneous_stride_length_supported: bool 

29 total_distance_supported: bool 

30 walking_or_running_status_supported: bool 

31 calibration_procedure_supported: bool 

32 multiple_sensor_locations_supported: bool 

33 

34 

35class RSCFeatureCharacteristic(BaseCharacteristic[RSCFeatureData]): 

36 """RSC Feature characteristic (0x2A54). 

37 

38 Used to expose the supported features of an RSC sensor. 

39 Contains a 16-bit bitmask indicating supported measurement 

40 capabilities. 

41 """ 

42 

43 expected_length: int = 2 

44 min_length: int = 2 

45 

46 def _decode_value( 

47 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True 

48 ) -> RSCFeatureData: 

49 """Parse RSC feature data. 

50 

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

52 

53 Args: 

54 data: Raw bytearray from BLE characteristic. 

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

56 validate: Whether to validate ranges (default True) 

57 

58 Returns: 

59 RSCFeatureData containing parsed feature flags. 

60 

61 Raises: 

62 ValueError: If data format is invalid. 

63 

64 """ 

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

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

67 

68 # Parse feature flags according to specification 

69 return RSCFeatureData( 

70 features=RSCFeatures(feature_mask), 

71 instantaneous_stride_length_supported=bool( 

72 feature_mask & RSCFeatures.INSTANTANEOUS_STRIDE_LENGTH_SUPPORTED 

73 ), 

74 total_distance_supported=bool(feature_mask & RSCFeatures.TOTAL_DISTANCE_SUPPORTED), 

75 walking_or_running_status_supported=bool(feature_mask & RSCFeatures.WALKING_OR_RUNNING_STATUS_SUPPORTED), 

76 calibration_procedure_supported=bool(feature_mask & RSCFeatures.CALIBRATION_PROCEDURE_SUPPORTED), 

77 multiple_sensor_locations_supported=bool(feature_mask & RSCFeatures.MULTIPLE_SENSOR_LOCATIONS_SUPPORTED), 

78 ) 

79 

80 def _encode_value(self, data: RSCFeatureData) -> bytearray: 

81 """Encode RSC feature value back to bytes. 

82 

83 Args: 

84 data: RSCFeatureData containing RSC feature data 

85 

86 Returns: 

87 Encoded bytes representing the RSC features (uint16) 

88 

89 """ 

90 # Reconstruct the features bitmap from individual flags 

91 features_bitmap = 0 

92 if data.instantaneous_stride_length_supported: 

93 features_bitmap |= RSCFeatures.INSTANTANEOUS_STRIDE_LENGTH_SUPPORTED 

94 if data.total_distance_supported: 

95 features_bitmap |= RSCFeatures.TOTAL_DISTANCE_SUPPORTED 

96 if data.walking_or_running_status_supported: 

97 features_bitmap |= RSCFeatures.WALKING_OR_RUNNING_STATUS_SUPPORTED 

98 if data.calibration_procedure_supported: 

99 features_bitmap |= RSCFeatures.CALIBRATION_PROCEDURE_SUPPORTED 

100 if data.multiple_sensor_locations_supported: 

101 features_bitmap |= RSCFeatures.MULTIPLE_SENSOR_LOCATIONS_SUPPORTED 

102 

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