Skip to content

Virtual Shifting (VS) for Legacy KICKR Smart trainers that are deprived of Wahoo VS-enabling firmware update(s)

License

Notifications You must be signed in to change notification settings

Berg0162/Kickr-Virtual-Shifting

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

58 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ESP32 Icon Kickr-Virtual-Shifting

License: GPL v3 GitHub release (latest SemVer) Platform: ESP32 Platform: Zwift Platform: Rouvy GitHub issues GitHub Discussions

Virtual Shifting (VS) for Legacy Wahoo KICKR Smart trainers, that are deprived of the Wahoo VS-enabling firmware update.

🚴 What is Virtual Shifting (VS)?

Virtual Shifting lets you “change gears” on your smart trainer without touching your bike’s drivetrain.
Instead of physically moving your chain across cogs, software simulates different gear ratios and adjusts trainer resistance accordingly.

This feature is standard on newer Wahoo KICKR Smart trainers — but older models never received the firmware update. That’s where this project comes in.

Goal: Bring VS to legacy Wahoo KICKR Smart trainers so that cyclists can experience the benefits of virtual shifting without needing to buy new trainer hardware.

🔧 What do you need to try Virtual Shifting?

At the very minimum, you need:

  • A Wahoo KICKR Smart trainer (legacy model without VS support).
  • A Zwift Click device (traded by Zwift).
    • The Click is handy because it can be moved around and mounted in different spots on the handlebars and elsewhere. Notice that these are also offered in online marketplaces now, by people that have bought newer version(s) or a Zwift Play device.
      ❗NOTICE: You can test Kickr-Virtual-Shifting without having/using a Zwift Click device❗

👉 That’s it! No need for extra gadgets.

  • No Zwift Cog required — just align your bike chain in an optimal straight line, like in gear 34/17.
  • No Zwift Play required — although it will likely work as well if you own one.

This project bridges your trainer and Zwift so that pressing shift buttons on Zwift Click feels like changing gears on a VS-enabled trainer.

Note: This bridge only controls trainer resistance.
It does not modify or fake your power, cadence, heart rate, or Zwift ride data in any way.

🌟 Why this project?

Wahoo began deploying VS for compatible KICKR trainers in early 2024. This feature was initially made available through firmware updates for the KICKR Core, KICKR v6, and KICKR Move models in February 2024.
The idea behind this firmware update is that shifting is now handled by the trainer rather than by Zwift...

  • All wellknown trainer manufacturers have embraced Zwift Virtual Shifting, which changes the landscape considerably.
  • Many KICKR smart trainers are still working great but lack VS support (never got the firmware update!)
  • At minimal cost this project can unlock a similar experience and extend the lifespan of your trainer.
  • This repo provides an Arduino library + concise examples so you can try VS yourself.

🛠 Who is this for?

This project is written with (novice) programmers who are also cyclists in mind.
If you:

  • Have a KICKR smart trainer that is deprived of the Wahoo VS-enabling firmware update
  • Are curious about VS,
  • Know a little Arduino programming (or want to learn), then this repo is for you. 🚀

You don’t need to hack the internals — just upload the example sketche(s) and start experimenting.

📦 What you’ll find here

  • /src → the C++ library (building blocks for VS).
  • /src/config → configuration settings
  • /examples → ready-to-run demos showing how to connect and shift.
  • /docs → background info and board setup.
  • /media → applied images store.

🪄 How the code works?

At its core, this project acts as a bridge between Zwift Virtual Shifting (VS) and the legacy KICKR Smart trainer.
Zwift sends commands over BLE in formats that the KICKR Smart does not natively understand. The bridge intercepts these commands, interprets whether Zwift is asking for a target power, a road gradient, or a virtual gear change, and then translates them into the CPS- or FTMS-messages (optionally) that the trainer does support.
In this way, the legacy KICKR Smart trainer behaves as if it had native Zwift VS support, even though its firmware was never updated for it.

🚴 Supported Trainer Modes

The bridge implements three distinct modes that mirror how Zwift communicates with smart trainers.
Which mode is active is not set explicitly by Zwift, but is inferred from the type of data Zwift VS sends.

  • ERG_MODE
    When Zwift sends a Target Power command (e.g. during a workout block in a .zwo file), the bridge switches into ERG mode.
    In this mode the trainer is controlled purely by power targets: the KICKR Smart is instructed to hold the requested wattage, regardless of cadence, speed, or gear.

  • SIM_MODE
    When Zwift sends a grade value (zwiftGrade) without virtual shifting active, the bridge passes this value directly on to the trainer using CPS or FTMS control commands.
    The trainer then applies resistance according to the simulated slope, just like in a normal Zwift ride without VS enabled.
    ❗NOTICE: This mode enables you to test Kickr-Virtual-Shifting without having/using a Zwift Click device ❗

  • SIM_MODE + Virtual Shifting
    When Zwift VS sends a non-zero gear ratio, the bridge switches into Virtual Shifting mode.
    Here the raw grade from Zwift is not used directly; instead, the bridge calculates an effective grade that takes into account the rider’s cadence, the chosen virtual gear ratio, total weight and resistance forces.
    This reconstructed grade is then sent to CPS or FTMS Control Point, making the KICKR Smart behave as if it natively supported Zwift’s virtual gearing.

In short:

  • Target Power received → ERG mode
  • Grade received without gear ratio → SIM mode
  • Gear ratio received → Virtual Shifting mode

This automatic detection ensures that the KICKR Smart follows Zwift’s logic seamlessly, even though it never received official firmware support for Virtual Shifting.

📐 How is resistance grade calculated?

What is Virtual Shifting in reality?
Virtual Shifting does not change drivetrain physics in the trainer.
It does one thing only:

It changes the mechanical advantage perceived by the rider by modulating resistance.

On real bikes:

  • Gear ratio changes torque ↔ cadence relationship

On Wahoo trainers:

  • Torque + flywheel speed drive resistance
  • There is no drivetrain model

Therefore:

Virtual shifting on Wahoo trainers is resistance remapping, not drivetrain modeling.

This is exactly why Wahoo never needed cadence or gear ratio internally.
Choosing the mapping function is important!
You do not want a linear mapping — that feels wrong.
A good, stable model is logarithmic or power-based, because:

  • Real gear steps feel exponential
  • Small gear changes shouldn’t cause massive grade jumps

Practical and proven approach
gearFactor = (R / R_ref) ^ α

Where:

α ≈ 0.7 … 1.0 (tunable)

R_ref = Zwift’s “neutral” gear (typically middle of cassette)

Then:
effectiveGrade = realGrade × gearFactor

The workhorse function that does the core of the calculations, sits at the heart of the ESP32 Virtual Shifting → KICKR Smart bridge. The function/member is a.k.a. UTILS::calculateGearedZwiftGrade(..) and can be found for further inspection at /src/utilities.cpp. Its role is to take the information Zwift VS provides — gear ratio and grade — and translate it into something the KICKR Smart understands: a remapped resistance grade.

The end result is that, whether the rider is shifting gears, rolling down a virtual hill, or grinding up Alpe du Zwift, the KICKR Smart receives a smooth and believable track resistance signal. The trainer “thinks” it is simply following a gradient profile, while in reality it is being fed a carefully reconstructed version of Zwift’s virtual world.

For further reading: Alpha Explained

🔒 Accuracy and Transparency

A common concern when using a bridge or simulation algorithm is whether it might interfere with the key performance data that Zwift displays: power, cadence, heart rate, and speed.
It is important to stress that this project does not alter or fabricate any of these values.

  • Power (and optionally cadence) are always measured directly by the KICKR Smart trainer and reported unchanged to Zwift.
  • Heart rate is passed through directly from your sensor without modification.
  • Speed as shown in Zwift is determined by Zwift’s own physics engine (road gradient, drafting, and rider profile), not by the bridge.

What the bridge does is limited to resistance control only: it translates Zwift’s Virtual Shifting and gradient commands into control messages that the KICKR Smart can understand.
This affects how the trainer feels under your legs, but never the numbers that Zwift records or displays.

In short:
The bridge makes your KICKR Smart respond correctly to Zwift VS commands, but your power output and ride data remain 100% authentic and untouched.

📚 Dependencies

ESP32 MCU Hardware

  • Supported MCU's with NimBLE-Arduino
    • Espressif: ESP32, ESP32C3, ESP32S3

Software
This Kickr-Virtual-Shifting library relies on the following Arduino libraries:

You can install these directly through the Arduino IDE Library Manager or by cloning the repos to your Arduino/libraries folder.

🖥️ Supported KICKR Hardware

This project is designed for legacy Wahoo KICKR smart trainers that do not natively support VS.

Project needs a trainer that meets all of the following:

  1. BLE Cycling Power Service (CPS)–centric control

    • Uses Wahoo proprietary CPS Control Point
    • Responds to CPS grade/SimMode
    • Uses CPS simulation to effect resistance
  2. Historically pre-FTMS or early FTMS, but fully CPS-compliant

    • Exposes CPS grade resistance behavior
    • Optionally exposes FTMS only if it properly implements simulation
  3. Firmware versions that did not relegate CPS simulation to “ignored”

    • Older firmware era before Wahoo’s FTMS evolution
  4. No ZVS requirement

    • Project is targeting legacy clients and control paths

Here are the Wahoo KICKR models and their approximate eras — grouped by how likely they are to support true CPS simulation:

Tier A — Most Reliable for True CPS Simulation

These are the most obvious units: KICKR (Gen1) — Wahoo KICKR Classic

  • Production: ~2014–2017
  • Characteristics:
    • CPS-only control path
    • Resistance responds to grade commands
    • Proven CPS physics loop
    • No FTMS by default
  • Ideal for:
    • Legacy simulation
    • CPS-only

KICKR (Gen2) — Wahoo KICKR Classic with minor revisions

  • Production: ~2016–2018
  • Characteristics:
    • Same architecture as Gen1
    • Some units may have early FTMS but under-the-hood behavior is still CPS-dominant
    • Very likely to implement CPS simulation reliably
Tier B — Transitional but Still Usable (Subject to Firmware)

These units sometimes behave well, depending on firmware: KICKR Snap (Early Models)

  • Production: ~2016–2018
  • Middle-class direct-drive trainer
  • Known to implement CPS simulation
  • Often no FTMS or very early unfinished FTMS
  • Resistance control via legacy CPS works reliably

Beware:

  • Some later Snap firmwares may have partial FTMS behavior
Tier C — Mixed Behavior (Not Ideal, But Sometimes Works)

Less predictable — firmware matters: KICKR Core (Early)

  • Early FTMS support but may still honor CPS simulation depending on firmware version
  • Test on real hardware before committing

KICKR v4 / v5 early builds

  • Have FTMS by default
  • CPS is present but simulation behavior can be ignored or suppressed
  • Only use if you verify that CPS grade affects resistance
Tier D — Not Suitable for Legacy CPS Simulation

These do not meet requirement because CPS simulation is either ignored or not consistently implemented:

  • KICKR A9## — CPS exposed, but simulation is ignored
  • KICKR with modern ZVS-first firmware — all CPS simulation suppressed
  • Any KICKR post ~2020 with strong FTMS/ZVS dominance
  • Units that expect ZVS-native commands
Important Note About Firmware Versions

Even within a physical generation, firmware matters:

  • Older firmware builds (2015–2018 era) generally implement CPS resistance faithfully.
  • Later firmware (2019+) may have introduced partial FTMS and suppressed CPS simulation.
Recommendation Summary (in priority order)

Ideal “Legacy CPS Simulation” trainers:

  1. Wahoo KICKR (Gen1)
  2. Wahoo KICKR (Gen2)
  3. Early Wahoo KICKR Snap

Secondary candidates (verify first):

  • Early Wahoo KICKR Core
  • Early FTMS transitional units

Avoid or verify carefully:

  • KICKR A9 and newer
  • KICKR v4/v5 with modern firmware
  • Trainers with ZVS-only prominence

⚠️ If you test a secondary model, please share your results in Discussions.

🔌 Tested Boards

This project has been tested successfully with the following ESP32 MCU boards:

  • Seeed Studio XIAO ESP32S3

XIAO


  • Adafruit Feather ESP32 V2

Feather


  • LilyGo_T-Dongle-S3

T-Dongle-S3



The boards work reliably with this project. Check pricing and availability!

Two boards have their own setup instructions in /docs to ensure correct configuration and library installation.

The selfcontained T-Dongle-S3 has a dedicated github repo

👉 Other ESP32-based boards may work as well, but are not tested.
If you try a different board, please share your results via Discussions

⚡ Getting Started

  1. Install Arduino IDE 2.xDownload here.
  2. Install the Kickr-Virtual-Shifting library from this repository. Download as .zip and extract to Arduino/libraries folder, or
    in Arduino IDE from Sketch menu -> Include library -> Add .Zip library
  3. Open Arduino IDE → load the examples/ sketches.
  4. Install required libraries using Arduino IDE Library Manager (see Dependencies).
  5. Setup IDE -> Tools menu for the type of board you are using!
Select and upload the example code to your ESP32 board
  1. Start the Serial Monitor to catch debugging info
  2. Start/Power-On the KICKR Smart Trainer
  3. Your ESP32 and Trainer will pair
  4. Start Zwift-App on your computer or tablet and wait....
  5. Search on the Zwift pairing screens for your ESP32 a.k.a. KICKRS
  6. Pair: Power and Controllable one after another with KICKRS
  7. Pair: Controls, your Zwift Click device and optionally others
  8. Pair: Heartrate (optionally)
  9. Select any Zwift ride you like
  10. Make Serial Monitor output window visible
  11. Hop on the bike: do the work and feel resistance change when shifting and with road inclination
  12. Inspect the info presented by Serial Monitor.....

This device is identified with the name KICKRS. You will see this only when connecting to Zwift on the pairing screens! Notice: Zwift extends device names with additional numbers for identification!

🪄 Central configuration

All configuration settings have been gathered in one config directory ../documents/arduino/libaries/KickrVirtualShifting/src/config for Debug-, NimBLE- and Kickr-configurations. Check this out: Configuring Kickr-Virtual-Shifting

🚴 ROUVY App Support

ROUVY

ROUVY is an indoor cycling platform focused on realistic routes, training, and events.
In February 2025, ROUVY added support for Zwift-style Virtual Shifting (VS), enabling interoperability with compatible hardware.

This Kickr-Virtual-Shifting library supports:

  • Zwift App
  • ROUVY App

Compatible hardware

  • Zwift Click shifter
  • Zwift Ride smart frame (integrated controllers)

Not supported by ROUVY at this time

  • Zwift Play controllers

No Zwift Cog is required; a fixed bike gear position (e.g. 34/17) is sufficient.

🙏 Credits

This project builds on the work of several excellent open-source projects.

  • SHIFTR — BLE to Direct Connect bridge for bike trainers adding virtual shifting for Zwift (GPL-3.0 License)
  • qdomyos-zwift — Zwift bridge for smart treadmills and bike/cyclette (GPL-3.0 License)
  • NimBLE-Arduino — Bluetooth Low Energy library (Apache 2.0 License)
  • uleb128 — Unsigned LEB128 encoding/decoding (MIT License)

Full attribution and license details are included in NOTICE.txt.

⚖️ License

This project is licensed under the GNU General Public License v3.0 (GPL-3.0).
You may freely use, modify, and distribute this project, provided that any derivative work is also licensed under GPL-3.0.

See the LICENSE file for full details.

🔧 Basic Usage

#include <KickrVirtualShifting.h>

void setup() { 
  // Init NimBLEManager 
  BLEmanager->init();
  // Start scanning for a trainer
  BLEmanager->startScanning();

  // Wait until the Peripheral/Trainer is successfuly connected or has a timeout
  const long TIMEOUT = millis() + 10000; // Within 10 seconds it should have found a KICKR trainer!
  while(!BLEmanager->clientIsConnected) {
    delay(100);
    if(millis() > TIMEOUT) {
      break;
    }
  }

  // KICKRS is connected with the Trainer, start advertising KICKRS for Zwift connection!
  BLEmanager->startAdvertising();
}

void loop() { }

⚠️ Disclaimer

💡 Research & Independence

This project is not affiliated with, endorsed by, or associated with any commercial cycling platform or trainer manufacturer.
It is a research and interoperability initiative designed to explore ways to increase the durability of legacy indoor bike trainers.
All development is conducted independently for educational and experimental purposes.

Compliance & Responsibility

This repository does not include or promote any circumvention of technological protection measures, reverse engineering of proprietary software, or unauthorized access to restricted systems.
Users are solely responsible for ensuring that their use of this code complies with local laws, software licenses, and platform terms of service.

🔍 Copyright & Contact

If you are a rights holder and believe that this project includes content that violates your intellectual property rights, please open a new issue on this repository to initiate a respectful review.
We are committed to responding promptly and, if appropriate, taking corrective action.

❤️ Contributing

This project is just starting! If you’re interested in testing, coding, writing docs, or just giving feedback, contributions are welcome in Discussions.

⚖️ Legal Notice (EU Context)

This project is developed and published in accordance with EU directives that recognize the right to study, test, and develop software components for the purpose of achieving interoperability (e.g., Directive 2009/24/EC on the legal protection of computer programs, Article 6).

No part of this project is intended to infringe upon intellectual property rights or violate technological protection measures. All content is shared in good faith under the belief that it falls within the bounds of legitimate research, reverse engineering for interoperability, and fair use under EU law.

Users must ensure their own compliance with national implementations of EU directives, and are responsible for how they apply or modify this code.