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 then set_wind_resistance and set_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 disable_fec_notifications()[source]
async enable_fec_notifications()[source]
async request_data_page(page_number)[source]
async set_basic_resistance(resistance)[source]

Activate basic resistance mode, with specified resistance

Parameters:

resistance – Resistance to apply to trainer, in newtons

set_command_status_data_page_handler(callback)[source]
set_general_fe_data_page_handler(callback)[source]
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!

set_specific_trainer_data_page_handler(callback)[source]
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

class pycycling.tacx_trainer_control.TargetPowerLimit(value, names=<not given>, *values, module=None, qualname=None, type=None, start=1, boundary=None)

Bases: Enum

limit_reached = 4
operating_at_target_or_no_target_set = 1
user_speed_too_high = 3
user_speed_too_low = 2