pycycling.tacx_trainer_control module¶
A module for interacting with Tacx Bluetooth smart turbo trainers.
This protocol is a variation of the ANT+ FE-C standard (but using BLE instead of ANT+) - Tacx released many of their trainers before the now common FTMS protocol was finalised.
The protocol also facilitates the use of the NEO Road Feel feature.
Smart trainer modes of operation¶
The FE-C standard defines a few modes of operation. To understand the difference between these, a little theory is required.
Fundamentally, when you cycle two types of forces come into play:
- Resistive forces:
These are typically rolling resistance, wind resistance, and gravitational resistance. They are forces that would bring you back to a stop if you stop cycling.
- Inertial forces:
These are the forces that resist changes to your velocity (think Newton’s second law). These forces work against you when you accelerate making it hard work to get going (and even harder the heavier you are). However, when you stop pedalling these forces work in your favour, and help counter the resistive forces which slow you down, allowing you to coast along for a while.
Many turbo trainers apply inertial forces through a heavy flywheel, and resistive forces through a brake device. The Tacx NEO is somewhat unique in that it applies both inertial and resistive forces through electromagnets.
The NEO has 5 different operational modes, which alter the application of these two types of forces. Other trainers support only the first 3. I’ll try and explain in a few words what each does:
- Basic resistance mode:
This is a simple mode, and not useful for many applications. You directly set the resistance force. It applies inertial forces assuming a small preset rider mass which means that this mode is not useful for a simulator (because it therefore applies the incorrect inertial forces for most riders). In pycycling you activate it by using
set_basic_resistance
.
- Simulation mode:
Here, the trainer computes the resistive force by internally computing a simple physics equation. The equation has parameters which can be configured such as wind speed and track incline. For the full details, please refer to the ANT+ FE-C specification. In this mode, the trainer dynamically adjusts the flywheel mass so it applies the correct inertial force for the rider. To activate this mode, first use
set_user_configuration
thenset_wind_resistance
andset_track_resistance
as required. I would strongly recommend using this for any simulator application and this is what is used by Zwift etc.
- Target power mode (erg mode):
The trainer adjusts the resistance based on the riders exertion in order to maintain a constant power output. Use
set_target_power
to activate.
- Isokinetic mode:
The trainer adjusts the resistance to maintain a constant cadence. See
set_neo_modes
for info on this.
- Isotonic mode:
This is a special case of basic resistance mode, where the flywheel simulated mass is set to zero rather than a small mass, meaning the inertial forces applied by the trainer are zero. To activate, set bike weight, user weight, incline, drag resistance and rolling resistance to zero and then use
set_basic_resistance
as before.
Example¶
This example demonstrates use of the basic resistance mode. Initially a resistance of 20 newtons is set, and then 20 seconds later this is adjusted to 40 newtons. Data from the trainer is also printed to the console. Please see also information on obtaining the Bluetooth address of your device.
import asyncio
from bleak import BleakClient
from pycycling.tacx_trainer_control import TacxTrainerControl
async def run(address):
async with BleakClient(address) as client:
def my_page_handler(data):
print(data)
await client.is_connected()
trainer = TacxTrainerControl(client)
trainer.set_specific_trainer_data_page_handler(my_page_handler)
trainer.set_general_fe_data_page_handler(my_page_handler)
await trainer.enable_fec_notifications()
await trainer.set_basic_resistance(20)
await asyncio.sleep(20.0)
await trainer.set_basic_resistance(40)
await asyncio.sleep(20.0)
await trainer.disable_fec_notifications()
if __name__ == "__main__":
import os
os.environ["PYTHONASYNCIODEBUG"] = str(1)
device_address = "EAAA3D1F-6760-4D77-961E-8DDAC1CC9AED"
loop = asyncio.get_event_loop()
loop.run_until_complete(run(device_address))
- class pycycling.tacx_trainer_control.CommandStatus(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)¶
Bases:
Enum
- fail = 2¶
- not_supported = 3¶
- rejected = 4¶
- success = 1¶
- uninitialized = 5¶
- class pycycling.tacx_trainer_control.CommandStatusData(last_received_command, command_status, data)¶
Bases:
tuple
- command_status¶
Alias for field number 1
- data¶
Alias for field number 2
- last_received_command¶
Alias for field number 0
- class pycycling.tacx_trainer_control.EquipmentType(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)¶
Bases:
Enum
- climber = 5¶
- elliptical = 2¶
- nordic_skier = 6¶
- reserved = 3¶
- rower = 4¶
- trainer = 7¶
- treadmill = 1¶
- class pycycling.tacx_trainer_control.FEState(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)¶
Bases:
Enum
- finished = 4¶
- in_use = 3¶
- ready = 2¶
- reserved = 1¶
- class pycycling.tacx_trainer_control.GeneralFEData(equipment_type, elapsed_time, distance_travelled, speed, heart_rate, fe_state, lap_toggle)¶
Bases:
tuple
- distance_travelled¶
Alias for field number 2
- elapsed_time¶
Alias for field number 1
- equipment_type¶
Alias for field number 0
- fe_state¶
Alias for field number 5
- heart_rate¶
Alias for field number 4
- lap_toggle¶
Alias for field number 6
- speed¶
Alias for field number 3
- class pycycling.tacx_trainer_control.RoadSurface(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)[source]¶
Bases:
Enum
Road surfaces supported by the NEO road feel feature
- BRICK_ROAD = 5¶
- CATTLE_GRID = 2¶
- COBBLESTONES_HARD = 3¶
- COBBLESTONES_SOFT = 4¶
- CONCRETE_PLATES = 1¶
- GRAVEL = 7¶
- ICE = 8¶
- OFF_ROAD = 6¶
- SIMULATION_OFF = 0¶
- WOODEN_BOARDS = 9¶
- class pycycling.tacx_trainer_control.SpecificTrainerData(update_event_count, instantaneous_cadence, accumulated_power, instantaneous_power, trainer_status, target_power_limits, fe_state, lap_toggle, power_calibration_required, resistance_calibration_required, user_configuration_required)¶
Bases:
tuple
- accumulated_power¶
Alias for field number 2
- fe_state¶
Alias for field number 6
- instantaneous_cadence¶
Alias for field number 1
- instantaneous_power¶
Alias for field number 3
- lap_toggle¶
Alias for field number 7
- power_calibration_required¶
Alias for field number 8
- resistance_calibration_required¶
Alias for field number 9
- target_power_limits¶
Alias for field number 5
- trainer_status¶
Alias for field number 4
- update_event_count¶
Alias for field number 0
- user_configuration_required¶
Alias for field number 10
- class pycycling.tacx_trainer_control.TacxTrainerControl(client)[source]¶
Bases:
object
- async set_basic_resistance(resistance)[source]¶
Activate basic resistance mode, with specified resistance
- Parameters:
resistance – Resistance to apply to trainer, in newtons
- async set_neo_modes(isokinetic_mode=False, isokinetic_speed=4.2, road_surface_pattern=RoadSurface.SIMULATION_OFF, road_surface_pattern_intensity=255)[source]¶
Set NEO specific parameters such as Road Feel mode and Isokinetic training mode
- Parameters:
isokinetic_mode – Enable isokinetic mode of the trainer
isokinetic_speed – The target speed used in isokinetic mode
road_surface_pattern – The road surface to be simulated
road_surface_pattern_intensity – The intensity of the feeling of the road surface. Note that even 50% feels fairly intense, 100% is untested and may damage the trainer!
- async set_target_power(target_power)[source]¶
Activate target power mode, with specified target power
- Parameters:
target_power – Target power, in watts
- async set_track_resistance(grade, coefficient_of_rolling_resistance)[source]¶
Activate simulation mode, specifying track resistance parameters
- Parameters:
grade – The grade (slope) of simulated track, in %.
coefficient_of_rolling_resistance – The coefficient of rolling resistance, in dimensionless units
- async set_user_configuration(user_weight, bicycle_weight, bicycle_wheel_diameter, gear_ratio)[source]¶
Configure trainer parameters, used when the trainer is in simulation mode
- Parameters:
user_weight – Weight of the user in kilograms
bicycle_weight – Weight of bicycle in kilograms
bicycle_wheel_diameter – Diameter of bike wheel, in metres
gear_ratio – The bike gear ratio (front chain ring teeth:rear wheel cog teeth)
- async set_wind_resistance(wind_resistance_coefficient, wind_speed, drafting_factor)[source]¶
Activate simulation mode, specifying wind parameters
- Parameters:
wind_resistance_coefficient – Wind resistance coefficient is the product of the frontal surface area, drag coefficient and air density of the simulation, in kg/m
wind_speed – Speed of wind acting on cyclist in simulation, in km/h. A positive value represents a head wind while a negative value represents a tail wind
drafting_factor – Use parameter to scale wind resistance to simulate drafting behind a virtual opponent