An ESP32 device that reads button events from Wahoo Kickr Bike shifter units and emulates a Zwift Click v2 BLE controller, enabling STI-feel shift paddles for Zwift virtual shifting with a Kickr Move + Zwift Cog setup.
Zwift's virtual shifting requires their Click v2 controllers — small rubber-band-mounted buttons that lack the tactile feel of real STI shifters. The Wahoo Kickr Bike has excellent shift paddles, but they only work with the Kickr Bike's own HCU. This bridge connects the two: plug in spare Kickr Bike shifters, and Zwift sees a Click v2 controller.
| Component | Qty | Notes |
|---|---|---|
| ESP32-S3 DevKitC (or similar) | 1 | Must support 2x I2C peripherals |
| 2.5mm TRRS panel-mount socket | 2 | For left and right shifters |
| USB-C breakout or direct power | 1 | Powers the ESP32 |
| Enclosure | 1 | 3D printed or off-the-shelf project box |
Each 2.5mm TRRS jack from the Kickr Bike shifter carries I2C:
| Jack Pin | Signal | ESP32 (Left) | ESP32 (Right) |
|---|---|---|---|
| Tip | 3.3V | 3V3 | 3V3 |
| Ring 1 | SCL | GPIO 9 | GPIO 11 |
| Ring 2 | SDA | GPIO 8 | GPIO 10 |
| Sleeve | GND | GND | GND |
Important: The shifters use 2.5mm TRRS jacks, not 3.5mm!
Each shifter contains a TCA8418 I2C keypad scanner at address 0x34. Since both share the same address, they must be on separate I2C buses (the ESP32 has two hardware I2C peripherals).
| Kickr Bike Button | Zwift Function |
|---|---|
| Right Gear Outer | Shift Up (+) — harder gear |
| Left Gear Outer | Shift Down (-) — easier gear |
| Right Inner | Steer Right / D-pad Right |
| Left Inner | Steer Left / D-pad Left |
| Right Forward | D-pad Up |
| Left Forward | D-pad Down |
| Right Gear Inner | A button (PowerUp / Ride On) |
| Left Gear Inner | B button |
The mapping is defined in buildZwiftBitmap() and can be changed to your preference.
- Install Arduino IDE 2.x
- Add ESP32 board support (Espressif Systems)
- Install the NimBLE-Arduino library via Library Manager
- Open
zwahoo.ino - Adjust the I2C pin definitions at the top if your board uses different GPIOs
- Select your ESP32-S3 board and upload
- Plug Kickr Bike shifters into the two 2.5mm sockets
- Power the ESP32 via USB-C
- Serial monitor will show detected shifters and button events
- In Zwift, go to the pairing screen → Controls → look for "Zwift SF2"
- Pair it — Zwift will perform the "RideOn" handshake
- Start riding with virtual shifting!
- Input: The ESP32 polls two TCA8418 I2C keypad scanners (one per shifter) for button press/release events via the key event FIFO
- Processing: Button events are mapped to the Zwift Click v2 button bitmap format
- Output: The ESP32 advertises as a Zwift-compatible BLE controller using the proprietary Zwift service UUID (0xFC82), responds to the "RideOn" handshake, and sends protobuf-encoded button state notifications at 10-50 Hz
- Untested — this is a v0.1 based on reverse-engineered protocols; it will likely need tuning
- No analog steering — the Click v2 protocol supports analog paddle values; this firmware only sends digital button states. Steering via D-pad arrows should still work.
- Handshake may need refinement — the exact bytes Zwift sends/expects during connection may vary between Zwift versions. Monitor the serial output and adjust if needed.
- Button mapping is guesswork — the exact Kickr Bike button-to-row mapping may need verification with your specific shifter hardware. Connect one shifter at a time and press each button while watching serial output.
- Service UUID versioning — if Zwift doesn't see the device, try the old 128-bit UUID instead of 0xFC82
- StephenDone/kickr_shifters — TCA8418 reverse engineering, pinout, cabling, and Python test script for Kickr Bike shifters
- Makinolo — Zwift Ride/Click v2 BLE protocol reverse engineering
- JuergenLeber/SHIFTR — ESP32 Zwift BLE protocol implementation reference
MIT