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
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-03 16:41 +0000
1"""Fitness Machine Control Point characteristic (0x2AD9)."""
3from __future__ import annotations
5from enum import IntEnum
7import msgspec
9from ..context import CharacteristicContext
10from .base import BaseCharacteristic
11from .utils import DataParser
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)
18class FitnessMachineControlPointOpCode(IntEnum):
19 """Fitness Machine Control Point operation codes per FTMS specification."""
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
45class FitnessMachineResultCode(IntEnum):
46 """Fitness Machine Control Point result codes per FTMS specification."""
48 SUCCESS = 0x01
49 OP_CODE_NOT_SUPPORTED = 0x02
50 INVALID_PARAMETER = 0x03
51 OPERATION_FAILED = 0x04
52 CONTROL_NOT_PERMITTED = 0x05
55class FitnessMachineControlPointData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods
56 """Parsed data from Fitness Machine Control Point characteristic.
58 The parameter field contains opcode-specific data as raw bytes,
59 or None for opcodes with no parameters.
60 """
62 op_code: FitnessMachineControlPointOpCode
63 parameter: bytes | None = None
64 response_op_code: FitnessMachineControlPointOpCode | None = None
65 result_code: FitnessMachineResultCode | None = None
68class FitnessMachineControlPointCharacteristic(BaseCharacteristic[FitnessMachineControlPointData]):
69 """Fitness Machine Control Point characteristic (0x2AD9).
71 org.bluetooth.characteristic.fitness_machine_control_point
73 Used for control and configuration of fitness machines.
74 Provides commands for starting, stopping, and setting targets.
75 """
77 min_length = 1
78 allow_variable_length = True
80 def _decode_value(
81 self, data: bytearray, ctx: CharacteristicContext | None = None, *, validate: bool = True
82 ) -> FitnessMachineControlPointData:
83 """Parse Fitness Machine Control Point data.
85 Format: Op Code (uint8) + optional parameter (variable).
87 Args:
88 data: Raw bytearray from BLE characteristic.
89 ctx: Optional CharacteristicContext (may be None).
90 validate: Whether to validate ranges (default True).
92 Returns:
93 FitnessMachineControlPointData containing parsed control point data.
95 """
96 op_code_raw = DataParser.parse_int8(data, 0, signed=False)
97 op_code = FitnessMachineControlPointOpCode(op_code_raw)
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 )
112 parameter = bytes(data[1:]) if len(data) > 1 else None
114 return FitnessMachineControlPointData(
115 op_code=op_code,
116 parameter=parameter,
117 )
119 def _encode_value(self, data: FitnessMachineControlPointData) -> bytearray:
120 """Encode Fitness Machine Control Point data to bytes.
122 Args:
123 data: FitnessMachineControlPointData instance.
125 Returns:
126 Encoded bytes representing the control point command.
128 """
129 if not isinstance(data, FitnessMachineControlPointData):
130 raise TypeError(f"Expected FitnessMachineControlPointData, got {type(data).__name__}")
132 result = bytearray([int(data.op_code)])
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)
142 return result