Coverage for src / bluetooth_sig / gatt / characteristics / cross_trainer_data.py: 83%

252 statements  

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

1"""Cross Trainer Data characteristic implementation. 

2 

3Implements the Cross Trainer Data characteristic (0x2ACE) from the Fitness 

4Machine Service. A 24-bit flags field (3 bytes) controls the presence of 

5optional data fields -- the widest flags field in the fitness machine set. 

6 

7Bit 0 ("More Data") uses **inverted logic**: when bit 0 is 0 the 

8Instantaneous Speed field IS present; when bit 0 is 1 it is absent. 

9All other presence bits use normal logic (1 = present). 

10 

11Bit 15 is a **semantic bit** (Movement Direction): 0 = Forward, 1 = Backward. 

12It does NOT gate any data fields. 

13 

14References: 

15 Bluetooth SIG Fitness Machine Service 1.0 

16 org.bluetooth.characteristic.cross_trainer_data (GSS YAML) 

17""" 

18 

19from __future__ import annotations 

20 

21from enum import IntFlag 

22 

23import msgspec 

24 

25from ..constants import SINT16_MAX, SINT16_MIN, UINT8_MAX, UINT16_MAX, UINT24_MAX 

26from ..context import CharacteristicContext 

27from .base import BaseCharacteristic 

28from .fitness_machine_common import ( 

29 MET_RESOLUTION, 

30 decode_elapsed_time, 

31 decode_energy_triplet, 

32 decode_heart_rate, 

33 decode_metabolic_equivalent, 

34 decode_remaining_time, 

35 encode_elapsed_time, 

36 encode_energy_triplet, 

37 encode_heart_rate, 

38 encode_metabolic_equivalent, 

39 encode_remaining_time, 

40) 

41from .utils import DataParser 

42 

43# Speed: M=1, d=-2, b=0 -> actual = raw / 100 km/h 

44_SPEED_RESOLUTION = 100.0 

45 

46# Stride Count: M=1, d=-1, b=0 -> actual = raw / 10 

47_STRIDE_COUNT_RESOLUTION = 10.0 

48 

49# Inclination: M=1, d=-1, b=0 -> actual = raw / 10 % 

50# Ramp Setting: M=1, d=-1, b=0 -> actual = raw / 10 degrees 

51_TENTH_RESOLUTION = 10.0 

52 

53# Resistance Level: M=1, d=1, b=0 -> actual = raw * 10 

54_RESISTANCE_RESOLUTION = 10.0 

55 

56 

57class CrossTrainerDataFlags(IntFlag): 

58 """Cross Trainer Data flags as per Bluetooth SIG specification. 

59 

60 24-bit flags field (3 bytes). Bit 0 uses inverted logic: 

61 0 = Instantaneous Speed present, 1 = absent. 

62 Bit 15 is a semantic modifier (Movement Direction), not a presence flag. 

63 """ 

64 

65 MORE_DATA = 0x000001 # Inverted: 0 -> Speed present, 1 -> absent 

66 AVERAGE_SPEED_PRESENT = 0x000002 

67 TOTAL_DISTANCE_PRESENT = 0x000004 

68 STEP_COUNT_PRESENT = 0x000008 

69 STRIDE_COUNT_PRESENT = 0x000010 

70 ELEVATION_GAIN_PRESENT = 0x000020 

71 INCLINATION_AND_RAMP_PRESENT = 0x000040 

72 RESISTANCE_LEVEL_PRESENT = 0x000080 

73 INSTANTANEOUS_POWER_PRESENT = 0x000100 

74 AVERAGE_POWER_PRESENT = 0x000200 

75 EXPENDED_ENERGY_PRESENT = 0x000400 

76 HEART_RATE_PRESENT = 0x000800 

77 METABOLIC_EQUIVALENT_PRESENT = 0x001000 

78 ELAPSED_TIME_PRESENT = 0x002000 

79 REMAINING_TIME_PRESENT = 0x004000 

80 MOVEMENT_DIRECTION_BACKWARD = 0x008000 # Semantic: 0=Forward, 1=Backward 

81 

82 

83class CrossTrainerData(msgspec.Struct, frozen=True, kw_only=True): # pylint: disable=too-few-public-methods,too-many-instance-attributes 

84 """Parsed data from Cross Trainer Data characteristic. 

85 

86 Attributes: 

87 flags: Raw 24-bit flags field. 

88 instantaneous_speed: Instantaneous speed in km/h (0.01 resolution). 

89 average_speed: Average speed in km/h (0.01 resolution). 

90 total_distance: Total distance in metres (uint24). 

91 steps_per_minute: Steps per minute. 

92 average_step_rate: Average step rate in steps/min. 

93 stride_count: Stride count (0.1 resolution, a stride is a pair of steps). 

94 positive_elevation_gain: Positive elevation gain in metres. 

95 negative_elevation_gain: Negative elevation gain in metres. 

96 inclination: Current inclination in % (0.1 resolution, signed). 

97 ramp_setting: Current ramp angle in degrees (0.1 resolution, signed). 

98 resistance_level: Resistance level (unitless, resolution 10). 

99 instantaneous_power: Instantaneous power in watts (signed). 

100 average_power: Average power in watts (signed). 

101 total_energy: Total expended energy in kcal. 

102 energy_per_hour: Expended energy per hour in kcal. 

103 energy_per_minute: Expended energy per minute in kcal. 

104 heart_rate: Heart rate in bpm. 

105 metabolic_equivalent: MET value (0.1 resolution). 

106 elapsed_time: Elapsed time in seconds. 

107 remaining_time: Remaining time in seconds. 

108 movement_direction_backward: True if movement is backward, False if forward. 

109 

110 """ 

111 

112 flags: CrossTrainerDataFlags 

113 instantaneous_speed: float | None = None 

114 average_speed: float | None = None 

115 total_distance: int | None = None 

116 steps_per_minute: int | None = None 

117 average_step_rate: int | None = None 

118 stride_count: float | None = None 

119 positive_elevation_gain: int | None = None 

120 negative_elevation_gain: int | None = None 

121 inclination: float | None = None 

122 ramp_setting: float | None = None 

123 resistance_level: float | None = None 

124 instantaneous_power: int | None = None 

125 average_power: int | None = None 

126 total_energy: int | None = None 

127 energy_per_hour: int | None = None 

128 energy_per_minute: int | None = None 

129 heart_rate: int | None = None 

130 metabolic_equivalent: float | None = None 

131 elapsed_time: int | None = None 

132 remaining_time: int | None = None 

133 movement_direction_backward: bool = False 

134 

135 def __post_init__(self) -> None: 

136 """Validate field ranges.""" 

137 if ( 

138 self.instantaneous_speed is not None 

139 and not 0.0 <= self.instantaneous_speed <= UINT16_MAX / _SPEED_RESOLUTION 

140 ): 

141 raise ValueError( 

142 f"Instantaneous speed must be 0.0-{UINT16_MAX / _SPEED_RESOLUTION}, got {self.instantaneous_speed}" 

143 ) 

144 if self.average_speed is not None and not 0.0 <= self.average_speed <= UINT16_MAX / _SPEED_RESOLUTION: 

145 raise ValueError(f"Average speed must be 0.0-{UINT16_MAX / _SPEED_RESOLUTION}, got {self.average_speed}") 

146 if self.total_distance is not None and not 0 <= self.total_distance <= UINT24_MAX: 

147 raise ValueError(f"Total distance must be 0-{UINT24_MAX}, got {self.total_distance}") 

148 if self.steps_per_minute is not None and not 0 <= self.steps_per_minute <= UINT16_MAX: 

149 raise ValueError(f"Steps per minute must be 0-{UINT16_MAX}, got {self.steps_per_minute}") 

150 if self.average_step_rate is not None and not 0 <= self.average_step_rate <= UINT16_MAX: 

151 raise ValueError(f"Average step rate must be 0-{UINT16_MAX}, got {self.average_step_rate}") 

152 if self.stride_count is not None and not 0.0 <= self.stride_count <= UINT16_MAX / _STRIDE_COUNT_RESOLUTION: 

153 raise ValueError( 

154 f"Stride count must be 0.0-{UINT16_MAX / _STRIDE_COUNT_RESOLUTION}, got {self.stride_count}" 

155 ) 

156 if self.positive_elevation_gain is not None and not 0 <= self.positive_elevation_gain <= UINT16_MAX: 

157 raise ValueError(f"Positive elevation must be 0-{UINT16_MAX}, got {self.positive_elevation_gain}") 

158 if self.negative_elevation_gain is not None and not 0 <= self.negative_elevation_gain <= UINT16_MAX: 

159 raise ValueError(f"Negative elevation must be 0-{UINT16_MAX}, got {self.negative_elevation_gain}") 

160 if ( 

161 self.inclination is not None 

162 and not SINT16_MIN / _TENTH_RESOLUTION <= self.inclination <= SINT16_MAX / _TENTH_RESOLUTION 

163 ): 

164 raise ValueError( 

165 f"Inclination must be {SINT16_MIN / _TENTH_RESOLUTION}-{SINT16_MAX / _TENTH_RESOLUTION}, " 

166 f"got {self.inclination}" 

167 ) 

168 if ( 

169 self.ramp_setting is not None 

170 and not SINT16_MIN / _TENTH_RESOLUTION <= self.ramp_setting <= SINT16_MAX / _TENTH_RESOLUTION 

171 ): 

172 raise ValueError( 

173 f"Ramp setting must be {SINT16_MIN / _TENTH_RESOLUTION}-{SINT16_MAX / _TENTH_RESOLUTION}, " 

174 f"got {self.ramp_setting}" 

175 ) 

176 if self.resistance_level is not None and not 0.0 <= self.resistance_level <= UINT8_MAX * _RESISTANCE_RESOLUTION: 

177 raise ValueError( 

178 f"Resistance level must be 0.0-{UINT8_MAX * _RESISTANCE_RESOLUTION}, got {self.resistance_level}" 

179 ) 

180 if self.instantaneous_power is not None and not SINT16_MIN <= self.instantaneous_power <= SINT16_MAX: 

181 raise ValueError(f"Instantaneous power must be {SINT16_MIN}-{SINT16_MAX}, got {self.instantaneous_power}") 

182 if self.average_power is not None and not SINT16_MIN <= self.average_power <= SINT16_MAX: 

183 raise ValueError(f"Average power must be {SINT16_MIN}-{SINT16_MAX}, got {self.average_power}") 

184 if self.total_energy is not None and not 0 <= self.total_energy <= UINT16_MAX: 

185 raise ValueError(f"Total energy must be 0-{UINT16_MAX}, got {self.total_energy}") 

186 if self.energy_per_hour is not None and not 0 <= self.energy_per_hour <= UINT16_MAX: 

187 raise ValueError(f"Energy per hour must be 0-{UINT16_MAX}, got {self.energy_per_hour}") 

188 if self.energy_per_minute is not None and not 0 <= self.energy_per_minute <= UINT8_MAX: 

189 raise ValueError(f"Energy per minute must be 0-{UINT8_MAX}, got {self.energy_per_minute}") 

190 if self.heart_rate is not None and not 0 <= self.heart_rate <= UINT8_MAX: 

191 raise ValueError(f"Heart rate must be 0-{UINT8_MAX}, got {self.heart_rate}") 

192 if self.metabolic_equivalent is not None and not 0.0 <= self.metabolic_equivalent <= UINT8_MAX / MET_RESOLUTION: 

193 raise ValueError( 

194 f"Metabolic equivalent must be 0.0-{UINT8_MAX / MET_RESOLUTION}, got {self.metabolic_equivalent}" 

195 ) 

196 if self.elapsed_time is not None and not 0 <= self.elapsed_time <= UINT16_MAX: 

197 raise ValueError(f"Elapsed time must be 0-{UINT16_MAX}, got {self.elapsed_time}") 

198 if self.remaining_time is not None and not 0 <= self.remaining_time <= UINT16_MAX: 

199 raise ValueError(f"Remaining time must be 0-{UINT16_MAX}, got {self.remaining_time}") 

200 

201 

202class CrossTrainerDataCharacteristic(BaseCharacteristic[CrossTrainerData]): 

203 """Cross Trainer Data characteristic (0x2ACE). 

204 

205 Used in the Fitness Machine Service to transmit cross trainer workout 

206 data. A 24-bit flags field (3 bytes) controls which optional fields 

207 are present -- the widest flags field in the fitness machine set. 

208 

209 Flag-bit assignments (from GSS YAML): 

210 Bit 0: More Data -- **inverted**: 0 -> Inst. Speed present, 1 -> absent 

211 Bit 1: Average Speed present 

212 Bit 2: Total Distance present 

213 Bit 3: Step Count present (gates Steps/Min + Avg Step Rate) 

214 Bit 4: Stride Count present 

215 Bit 5: Elevation Gain present (gates Pos + Neg) 

216 Bit 6: Inclination and Ramp Angle Setting present (gates 2 fields) 

217 Bit 7: Resistance Level present 

218 Bit 8: Instantaneous Power present 

219 Bit 9: Average Power present 

220 Bit 10: Expended Energy present (gates triplet: total + /hr + /min) 

221 Bit 11: Heart Rate present 

222 Bit 12: Metabolic Equivalent present 

223 Bit 13: Elapsed Time present 

224 Bit 14: Remaining Time present 

225 Bit 15: Movement Direction (0=Forward, 1=Backward) -- semantic, not presence 

226 Bits 16-23: Reserved for Future Use 

227 

228 """ 

229 

230 expected_type = CrossTrainerData 

231 min_length: int = 3 # Flags only (24-bit = 3 bytes) 

232 allow_variable_length: bool = True 

233 

234 def _decode_value( 

235 self, 

236 data: bytearray, 

237 ctx: CharacteristicContext | None = None, 

238 *, 

239 validate: bool = True, 

240 ) -> CrossTrainerData: 

241 """Parse Cross Trainer Data from raw BLE bytes. 

242 

243 Args: 

244 data: Raw bytearray from BLE characteristic. 

245 ctx: Optional context (unused). 

246 validate: Whether to validate ranges. 

247 

248 Returns: 

249 CrossTrainerData with all present fields populated. 

250 

251 """ 

252 flags = CrossTrainerDataFlags(DataParser.parse_int24(data, 0, signed=False)) 

253 offset = 3 

254 

255 # Bit 0 -- inverted: Instantaneous Speed present when bit is NOT set 

256 instantaneous_speed = None 

257 if not (flags & CrossTrainerDataFlags.MORE_DATA) and len(data) >= offset + 2: 

258 raw_speed = DataParser.parse_int16(data, offset, signed=False) 

259 instantaneous_speed = raw_speed / _SPEED_RESOLUTION 

260 offset += 2 

261 

262 # Bit 1 -- Average Speed 

263 average_speed = None 

264 if (flags & CrossTrainerDataFlags.AVERAGE_SPEED_PRESENT) and len(data) >= offset + 2: 

265 raw_avg_speed = DataParser.parse_int16(data, offset, signed=False) 

266 average_speed = raw_avg_speed / _SPEED_RESOLUTION 

267 offset += 2 

268 

269 # Bit 2 -- Total Distance (uint24) 

270 total_distance = None 

271 if (flags & CrossTrainerDataFlags.TOTAL_DISTANCE_PRESENT) and len(data) >= offset + 3: 

272 total_distance = DataParser.parse_int24(data, offset, signed=False) 

273 offset += 3 

274 

275 # Bit 3 -- Steps Per Minute (uint16) + Average Step Rate (uint16) 

276 steps_per_minute = None 

277 average_step_rate = None 

278 if (flags & CrossTrainerDataFlags.STEP_COUNT_PRESENT) and len(data) >= offset + 4: 

279 steps_per_minute = DataParser.parse_int16(data, offset, signed=False) 

280 offset += 2 

281 average_step_rate = DataParser.parse_int16(data, offset, signed=False) 

282 offset += 2 

283 

284 # Bit 4 -- Stride Count (uint16, d=-1 -> raw/10) 

285 stride_count = None 

286 if (flags & CrossTrainerDataFlags.STRIDE_COUNT_PRESENT) and len(data) >= offset + 2: 

287 raw_stride = DataParser.parse_int16(data, offset, signed=False) 

288 stride_count = raw_stride / _STRIDE_COUNT_RESOLUTION 

289 offset += 2 

290 

291 # Bit 5 -- Positive Elevation Gain (uint16) + Negative Elevation Gain (uint16) 

292 positive_elevation_gain = None 

293 negative_elevation_gain = None 

294 if (flags & CrossTrainerDataFlags.ELEVATION_GAIN_PRESENT) and len(data) >= offset + 4: 

295 positive_elevation_gain = DataParser.parse_int16(data, offset, signed=False) 

296 offset += 2 

297 negative_elevation_gain = DataParser.parse_int16(data, offset, signed=False) 

298 offset += 2 

299 

300 # Bit 6 -- Inclination (sint16, d=-1) + Ramp Setting (sint16, d=-1) 

301 inclination = None 

302 ramp_setting = None 

303 if (flags & CrossTrainerDataFlags.INCLINATION_AND_RAMP_PRESENT) and len(data) >= offset + 4: 

304 raw_incl = DataParser.parse_int16(data, offset, signed=True) 

305 inclination = raw_incl / _TENTH_RESOLUTION 

306 offset += 2 

307 raw_ramp = DataParser.parse_int16(data, offset, signed=True) 

308 ramp_setting = raw_ramp / _TENTH_RESOLUTION 

309 offset += 2 

310 

311 # Bit 7 -- Resistance Level (uint8, d=1 -> raw * 10) 

312 resistance_level = None 

313 if (flags & CrossTrainerDataFlags.RESISTANCE_LEVEL_PRESENT) and len(data) >= offset + 1: 

314 raw_resistance = DataParser.parse_int8(data, offset, signed=False) 

315 resistance_level = raw_resistance * _RESISTANCE_RESOLUTION 

316 offset += 1 

317 

318 # Bit 8 -- Instantaneous Power (sint16) 

319 instantaneous_power = None 

320 if (flags & CrossTrainerDataFlags.INSTANTANEOUS_POWER_PRESENT) and len(data) >= offset + 2: 

321 instantaneous_power = DataParser.parse_int16(data, offset, signed=True) 

322 offset += 2 

323 

324 # Bit 9 -- Average Power (sint16) 

325 average_power = None 

326 if (flags & CrossTrainerDataFlags.AVERAGE_POWER_PRESENT) and len(data) >= offset + 2: 

327 average_power = DataParser.parse_int16(data, offset, signed=True) 

328 offset += 2 

329 

330 # Bit 10 -- Energy triplet (Total + Per Hour + Per Minute) 

331 total_energy = None 

332 energy_per_hour = None 

333 energy_per_minute = None 

334 if flags & CrossTrainerDataFlags.EXPENDED_ENERGY_PRESENT: 

335 total_energy, energy_per_hour, energy_per_minute, offset = decode_energy_triplet(data, offset) 

336 

337 # Bit 11 -- Heart Rate 

338 heart_rate = None 

339 if flags & CrossTrainerDataFlags.HEART_RATE_PRESENT: 

340 heart_rate, offset = decode_heart_rate(data, offset) 

341 

342 # Bit 12 -- Metabolic Equivalent 

343 metabolic_equivalent = None 

344 if flags & CrossTrainerDataFlags.METABOLIC_EQUIVALENT_PRESENT: 

345 metabolic_equivalent, offset = decode_metabolic_equivalent(data, offset) 

346 

347 # Bit 13 -- Elapsed Time 

348 elapsed_time = None 

349 if flags & CrossTrainerDataFlags.ELAPSED_TIME_PRESENT: 

350 elapsed_time, offset = decode_elapsed_time(data, offset) 

351 

352 # Bit 14 -- Remaining Time 

353 remaining_time = None 

354 if flags & CrossTrainerDataFlags.REMAINING_TIME_PRESENT: 

355 remaining_time, offset = decode_remaining_time(data, offset) 

356 

357 # Bit 15 -- Movement Direction (semantic, no data fields) 

358 movement_direction_backward = bool(flags & CrossTrainerDataFlags.MOVEMENT_DIRECTION_BACKWARD) 

359 

360 return CrossTrainerData( 

361 flags=flags, 

362 instantaneous_speed=instantaneous_speed, 

363 average_speed=average_speed, 

364 total_distance=total_distance, 

365 steps_per_minute=steps_per_minute, 

366 average_step_rate=average_step_rate, 

367 stride_count=stride_count, 

368 positive_elevation_gain=positive_elevation_gain, 

369 negative_elevation_gain=negative_elevation_gain, 

370 inclination=inclination, 

371 ramp_setting=ramp_setting, 

372 resistance_level=resistance_level, 

373 instantaneous_power=instantaneous_power, 

374 average_power=average_power, 

375 total_energy=total_energy, 

376 energy_per_hour=energy_per_hour, 

377 energy_per_minute=energy_per_minute, 

378 heart_rate=heart_rate, 

379 metabolic_equivalent=metabolic_equivalent, 

380 elapsed_time=elapsed_time, 

381 remaining_time=remaining_time, 

382 movement_direction_backward=movement_direction_backward, 

383 ) 

384 

385 def _encode_value(self, data: CrossTrainerData) -> bytearray: # noqa: PLR0912 

386 """Encode CrossTrainerData back to BLE bytes. 

387 

388 Reconstructs 24-bit flags from present fields so round-trip encoding 

389 preserves the original wire format. 

390 

391 Args: 

392 data: CrossTrainerData instance. 

393 

394 Returns: 

395 Encoded bytearray matching the BLE wire format. 

396 

397 """ 

398 flags = CrossTrainerDataFlags(0) 

399 

400 # Bit 0 -- inverted: set MORE_DATA when Speed is absent 

401 if data.instantaneous_speed is None: 

402 flags |= CrossTrainerDataFlags.MORE_DATA 

403 if data.average_speed is not None: 

404 flags |= CrossTrainerDataFlags.AVERAGE_SPEED_PRESENT 

405 if data.total_distance is not None: 

406 flags |= CrossTrainerDataFlags.TOTAL_DISTANCE_PRESENT 

407 if data.steps_per_minute is not None: 

408 flags |= CrossTrainerDataFlags.STEP_COUNT_PRESENT 

409 if data.stride_count is not None: 

410 flags |= CrossTrainerDataFlags.STRIDE_COUNT_PRESENT 

411 if data.positive_elevation_gain is not None: 

412 flags |= CrossTrainerDataFlags.ELEVATION_GAIN_PRESENT 

413 if data.inclination is not None: 

414 flags |= CrossTrainerDataFlags.INCLINATION_AND_RAMP_PRESENT 

415 if data.resistance_level is not None: 

416 flags |= CrossTrainerDataFlags.RESISTANCE_LEVEL_PRESENT 

417 if data.instantaneous_power is not None: 

418 flags |= CrossTrainerDataFlags.INSTANTANEOUS_POWER_PRESENT 

419 if data.average_power is not None: 

420 flags |= CrossTrainerDataFlags.AVERAGE_POWER_PRESENT 

421 if data.total_energy is not None: 

422 flags |= CrossTrainerDataFlags.EXPENDED_ENERGY_PRESENT 

423 if data.heart_rate is not None: 

424 flags |= CrossTrainerDataFlags.HEART_RATE_PRESENT 

425 if data.metabolic_equivalent is not None: 

426 flags |= CrossTrainerDataFlags.METABOLIC_EQUIVALENT_PRESENT 

427 if data.elapsed_time is not None: 

428 flags |= CrossTrainerDataFlags.ELAPSED_TIME_PRESENT 

429 if data.remaining_time is not None: 

430 flags |= CrossTrainerDataFlags.REMAINING_TIME_PRESENT 

431 if data.movement_direction_backward: 

432 flags |= CrossTrainerDataFlags.MOVEMENT_DIRECTION_BACKWARD 

433 

434 result = DataParser.encode_int24(int(flags), signed=False) 

435 

436 if data.instantaneous_speed is not None: 

437 raw_speed = round(data.instantaneous_speed * _SPEED_RESOLUTION) 

438 result.extend(DataParser.encode_int16(raw_speed, signed=False)) 

439 if data.average_speed is not None: 

440 raw_avg_speed = round(data.average_speed * _SPEED_RESOLUTION) 

441 result.extend(DataParser.encode_int16(raw_avg_speed, signed=False)) 

442 if data.total_distance is not None: 

443 result.extend(DataParser.encode_int24(data.total_distance, signed=False)) 

444 if data.steps_per_minute is not None: 

445 result.extend(DataParser.encode_int16(data.steps_per_minute, signed=False)) 

446 if data.average_step_rate is not None: 

447 result.extend(DataParser.encode_int16(data.average_step_rate, signed=False)) 

448 if data.stride_count is not None: 

449 raw_stride = round(data.stride_count * _STRIDE_COUNT_RESOLUTION) 

450 result.extend(DataParser.encode_int16(raw_stride, signed=False)) 

451 if data.positive_elevation_gain is not None: 

452 result.extend(DataParser.encode_int16(data.positive_elevation_gain, signed=False)) 

453 if data.negative_elevation_gain is not None: 

454 result.extend(DataParser.encode_int16(data.negative_elevation_gain, signed=False)) 

455 if data.inclination is not None: 

456 raw_incl = round(data.inclination * _TENTH_RESOLUTION) 

457 result.extend(DataParser.encode_int16(raw_incl, signed=True)) 

458 if data.ramp_setting is not None: 

459 raw_ramp = round(data.ramp_setting * _TENTH_RESOLUTION) 

460 result.extend(DataParser.encode_int16(raw_ramp, signed=True)) 

461 if data.resistance_level is not None: 

462 raw_resistance = round(data.resistance_level / _RESISTANCE_RESOLUTION) 

463 result.extend(DataParser.encode_int8(raw_resistance, signed=False)) 

464 if data.instantaneous_power is not None: 

465 result.extend(DataParser.encode_int16(data.instantaneous_power, signed=True)) 

466 if data.average_power is not None: 

467 result.extend(DataParser.encode_int16(data.average_power, signed=True)) 

468 if data.total_energy is not None: 

469 result.extend(encode_energy_triplet(data.total_energy, data.energy_per_hour, data.energy_per_minute)) 

470 if data.heart_rate is not None: 

471 result.extend(encode_heart_rate(data.heart_rate)) 

472 if data.metabolic_equivalent is not None: 

473 result.extend(encode_metabolic_equivalent(data.metabolic_equivalent)) 

474 if data.elapsed_time is not None: 

475 result.extend(encode_elapsed_time(data.elapsed_time)) 

476 if data.remaining_time is not None: 

477 result.extend(encode_remaining_time(data.remaining_time)) 

478 

479 return result