Zwift Trainer protocol

      Comments Off on Zwift Trainer protocol

To add a bit more complexity to the already messy landscape of cycling trainer protocols, Zwift decided a few years ago to create their own control protocol for the trainers they began manufacturing. This allowed them to address some of the shortcomings in FTMS while also freeing themselves from the standard, enabling them to incorporate new functionalities.

I don’t completely blame them, as the Bluetooth consortium is notoriously slow in updating standards, so it made sense. However, Zwift has not released this protocol to the public, allowing them to maintain exclusivity over the new features. As a result, no other cycling training software can interface with their latest hardware, effectively making their hardware customers captive.

I’ve written in the past about the Zwift BLE Service in my posts about Zwift Play, Click and Ride. The service to control and get data from the trainers Zwift Hub Classic, Zwift Hub One, Wahoo Kickr Core and recently Jetblack Victory, is the same. Advertising packets for the Hub I presume are the same as the ones for the other hardware devices the company sells, but haven’t had the opportunity to test that trainer, as all my rev-eng has taken place in a Wahoo Kickr Core. The handshake is almost identical as the Zwift Ride.

These trainers do not encrypt their messages, just like the Zwift Ride, and the protocol buffers messages they send have different ids or op codes than the controller devices.

Messages

The message ids for the trainer messages are basically 2. Lets remember that the information incoming in every notification or outgoing as a command, is a message code followed by the encoded protocol buffers message corresponding to that message code or id.

Riding Data (0x03)

Message 0x03 is the riding data, it contains the power and cadence matching exactly what is being broadcast over the CPS and FTMS services. Next is the speed, which value differs from FTMS (my guess being this speed the virtual speed calculated by the trainer as opposed to the real wheel speed broadcasted by FTMS), followed by the re-broadcast of the heart rate if you have your HR strap paired to the trainer, and 2 more magnitudes which I was unable to figure out during my BLE sniffing.

This message is received as a periodic notification in the notifications characteristic 00000002-19ca-4651-86e5-fa29dcdd09d1

// The command code prepending this message is 0x03
message HubRidingData {
	optional uint32 Power = 1;
	optional uint32 Cadence = 2;
	optional uint32 SpeedX100 = 3;
	optional uint32 HR = 4;
	optional uint32 Unknown1 = 5;	// Values observed 0 when stopped, 2864, 4060, 4636, 6803
	optional uint32 Unknown2 = 6;	// Values observed 25714 (constant during session)
}

For example

03 08 be 01 10 50 18 bd 06 20 00 28 e2 ba 01 30 8b eb 01

results in this decoded protocol buffers message

{
  "Power": 190,
  "Cadence": 80,
  "SpeedX100": 829,
  "HR": 0,
  "Unknown1": 23906,
  "Unknown2": 30091
}

Trainer Control (0x04)

The other important message is 0x04. This is the control message. It is sent to the Control Point Characteristic 00000003-19ca-4651-86e5-fa29dcdd09d1 and can contain any of the possible parameters to control a trainer, be it in target power mode, simulation mode, virtual gears and user information.

// The command code prepending this message is 0x04
message HubCommand {
	optional uint32 PowerTarget = 3;
	optional SimulationParam Simulation = 4;
	optional PhysicalParam Physical = 5;
}

As we can see, the message can contain optionally a Power Target, Simulation parameters or physical information on the biker and bike. These are the definitions of those 2 extra messages.

message SimulationParam {
	optional sint32 Wind = 1; // Wind in m/s * 100. Zwift fixes to 0. Negative is backwind
	optional sint32 InclineX100 = 2;   // Incline value * 100  
	optional uint32 CWa = 3; // Aero coefficient CW * a * 10000. Zwift fixes to 0.51 (5100) 
	optional uint32 Crr = 4; // Rolling resistance Crr * 100000. Zwift fixes to 0.004 (400)
}

message PhysicalParam {
	optional uint32 GearRatioX10000 = 2;
	optional uint32 BikeWeightx100 = 4;
	optional uint32 RiderWeightx100 = 5;
}

So an example of such a message will be to write to the control point

04 2a 08 10 00 20 81 05 28 c4 3b

deserializing this wire message to protocol buffers definition

{
  "Physical": {
    "BikeWeightx100": 641,
    "RiderWeightx100": 7620
  }
}

We are setting the weight of the bike (6.41Kg) and the user (76.2Kg)

Be especially cautious when setting the coefficients for rolling resistance and aerodynamics. If you input values that are significantly different from what the trainer expects, it will start behaving in very strange ways. I spent a week scratching my head, wondering why the trainer would sometimes respond to changes in gears and slope but never provide an accurate power load. It turned out I hadn’t properly scaled the values for the two coefficients. Note that the scaling is different from what’s used in FTMS, which is what led to my error.

It’s also apparent that there’s one field ID missing in this message definition. My guess is that field ‘1’ represents the parameter for resistance mode, where the trainer sets a percentage of its total resistance. It didn’t appear in my BLE traffic captures because Zwift—or anyone else—doesn’t use that mode with the trainer. I didn’t bother testing it for the same reason

Request for information (0x0)

Expanding a little bit on what I already found out in my investigation for the Zwift Ride, the command 0 can have parameters. In the case of the Wahoo Kickr Core, when you send the value 520 it replies with the current gear ratio. Values from 1 to 7 return the current values for the specific fields that are returned when the general info request is made (value 0). There are more values for the parameter that return information, but I was unable to identify what they meant.

Final thoughts

Now that Zwift is out of the trainer manufacturing business, I hope this Bluetooth service stops evolving into an even bigger monster. Nothing good comes from proprietary protocols. I also hope this post helps other software manufacturers support users who are currently locked into using the Zwift application.

It’s also worth noting how Zwift underutilizes the capabilities of trainers. For instance, the Zwift app does not adjust rolling resistance based on terrain, aerodynamic drag based on the bike, rider’s height, or cycling kit, nor does it factor in wind speed. While these elements are considered in their virtual speed calculations for the game, they don’t influence the resistance of the trainer itself, which takes away from the overall simulation experience