Coverage for src / bluetooth_sig / gatt / characteristics / fitness_machine_control_point.py: 99%

70 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-03 16:41 +0000

1"""Fitness Machine Control Point characteristic (0x2AD9).""" 

2 

3from __future__ import annotations 

4 

5from enum import IntEnum 

6 

7import msgspec 

8 

9from ..context import CharacteristicContext 

10from .base import BaseCharacteristic 

11from .utils import DataParser 

12 

13# Minimum data lengths for response parsing 

14_RESPONSE_OP_CODE_LENGTH = 2 # op_code(1) + response_op_code(1) 

15_RESPONSE_RESULT_LENGTH = 3 # op_code(1) + response_op_code(1) + result_code(1) 

16 

17 

18class FitnessMachineControlPointOpCode(IntEnum): 

19 """Fitness Machine Control Point operation codes per FTMS specification.""" 

20 

21 REQUEST_CONTROL = 0x00 

22 RESET = 0x01 

23 SET_TARGET_SPEED = 0x02 

24 SET_TARGET_INCLINATION = 0x03 

25 SET_TARGET_RESISTANCE_LEVEL = 0x04 

26 SET_TARGET_POWER = 0x05 

27 SET_TARGET_HEART_RATE = 0x06 

28 START_OR_RESUME = 0x07 

29 STOP_OR_PAUSE = 0x08 

30 SET_TARGETED_EXPENDED_ENERGY = 0x09 

31 SET_TARGETED_NUMBER_OF_STEPS = 0x0A 

32 SET_TARGETED_NUMBER_OF_STRIDES = 0x0B 

33 SET_TARGETED_DISTANCE = 0x0C 

34 SET_TARGETED_TRAINING_TIME = 0x0D 

35 SET_TARGETED_TIME_IN_TWO_HEART_RATE_ZONES = 0x0E 

36 SET_TARGETED_TIME_IN_THREE_HEART_RATE_ZONES = 0x0F 

37 SET_TARGETED_TIME_IN_FIVE_HEART_RATE_ZONES = 0x10 

38 SET_INDOOR_BIKE_SIMULATION_PARAMETERS = 0x11 

39 SET_WHEEL_CIRCUMFERENCE = 0x12 

40 SPIN_DOWN_CONTROL = 0x13 

41 SET_TARGETED_CADENCE = 0x14 

42 RESPONSE_CODE = 0x80 

43 

44 

45class FitnessMachineResultCode(IntEnum): 

46 """Fitness Machine Control Point result codes per FTMS specification.""" 

47 

48 SUCCESS = 0x01 

49 OP_CODE_NOT_SUPPORTED = 0x02 

50 INVALID_PARAMETER = 0x03 

51 OPERATION_FAILED = 0x04 

52 CONTROL_NOT_PERMITTED = 0x05 

53 

54 

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

56 """Parsed data from Fitness Machine Control Point characteristic. 

57 

58 The parameter field contains opcode-specific data as raw bytes, 

59 or None for opcodes with no parameters. 

60 """ 

61 

62 op_code: FitnessMachineControlPointOpCode 

63 parameter: bytes | None = None 

64 response_op_code: FitnessMachineControlPointOpCode | None = None 

65 result_code: FitnessMachineResultCode | None = None 

66 

67 

68class FitnessMachineControlPointCharacteristic(BaseCharacteristic[FitnessMachineControlPointData]): 

69 """Fitness Machine Control Point characteristic (0x2AD9). 

70 

71 org.bluetooth.characteristic.fitness_machine_control_point 

72 

73 Used for control and configuration of fitness machines. 

74 Provides commands for starting, stopping, and setting targets. 

75 """ 

76 

77 min_length = 1 

78 allow_variable_length = True 

79 

80 def _decode_value( 

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

82 ) -> FitnessMachineControlPointData: 

83 """Parse Fitness Machine Control Point data. 

84 

85 Format: Op Code (uint8) + optional parameter (variable). 

86 

87 Args: 

88 data: Raw bytearray from BLE characteristic. 

89 ctx: Optional CharacteristicContext (may be None). 

90 validate: Whether to validate ranges (default True). 

91 

92 Returns: 

93 FitnessMachineControlPointData containing parsed control point data. 

94 

95 """ 

96 op_code_raw = DataParser.parse_int8(data, 0, signed=False) 

97 op_code = FitnessMachineControlPointOpCode(op_code_raw) 

98 

99 if op_code == FitnessMachineControlPointOpCode.RESPONSE_CODE: 

100 response_op_code = None 

101 result_code = None 

102 if len(data) >= _RESPONSE_OP_CODE_LENGTH: 

103 response_op_code = FitnessMachineControlPointOpCode(DataParser.parse_int8(data, 1, signed=False)) 

104 if len(data) >= _RESPONSE_RESULT_LENGTH: 

105 result_code = FitnessMachineResultCode(DataParser.parse_int8(data, 2, signed=False)) 

106 return FitnessMachineControlPointData( 

107 op_code=op_code, 

108 response_op_code=response_op_code, 

109 result_code=result_code, 

110 ) 

111 

112 parameter = bytes(data[1:]) if len(data) > 1 else None 

113 

114 return FitnessMachineControlPointData( 

115 op_code=op_code, 

116 parameter=parameter, 

117 ) 

118 

119 def _encode_value(self, data: FitnessMachineControlPointData) -> bytearray: 

120 """Encode Fitness Machine Control Point data to bytes. 

121 

122 Args: 

123 data: FitnessMachineControlPointData instance. 

124 

125 Returns: 

126 Encoded bytes representing the control point command. 

127 

128 """ 

129 if not isinstance(data, FitnessMachineControlPointData): 

130 raise TypeError(f"Expected FitnessMachineControlPointData, got {type(data).__name__}") 

131 

132 result = bytearray([int(data.op_code)]) 

133 

134 if data.op_code == FitnessMachineControlPointOpCode.RESPONSE_CODE: 

135 if data.response_op_code is not None: 

136 result.append(int(data.response_op_code)) 

137 if data.result_code is not None: 

138 result.append(int(data.result_code)) 

139 elif data.parameter is not None: 

140 result.extend(data.parameter) 

141 

142 return result