Skip to content

Repository files navigation

BikeStation

Licence: GPL-3.0-or-later Platform: Windows Python 3.10+ Download

BikeStation

A local Windows dashboard for Echelon Connect bikes. It speaks the Echelon proprietary Bluetooth-LE protocol directly — no Zwift, no Echelon subscription, no cloud, no phone. Everything runs on your laptop and every ride is saved to disk.

The RIDE tab: power, cadence and resistance gauges with the session clock and resistance control

Download it (no Python, no setup)

Before you start, you need: a Windows PC with Bluetooth, and an Echelon bike whose Bluetooth name starts with ECH (see Compatibility). Do not pair the bike in Windows Bluetooth settings — BikeStation connects on its own, and pairing it in Windows can stop it working.

Step 1 — Download

That one file is the whole program. Nothing else to install.

Step 2 — Move it somewhere of its own

Drag it out of Downloads into its own folder, such as Documents\BikeStation. It saves your rides and settings automatically, so avoid Program Files, which Windows keeps read-only.

Step 3 — Expect a warning the first time

Windows shows a blue "Windows protected your PC" box. This appears for any program without a paid commercial signing certificate — it does not mean anything is wrong.

Click More info, then Run anyway.

Step 4 — Open it

Double-click BikeStation.exe. Nothing seems to happen for a second or two, then your web browser opens by itself showing the dashboard. That browser page is the app.

To stop it later, open Task Manager (Ctrl+Shift+Esc), find BikeStation, and press End task. Closing the browser tab alone leaves it running in the background.

Step 5 — Connect your bike

  1. Pedal the bike once to wake it up, and fully close the Echelon phone app — the bike allows only one connection at a time.
  2. In the dashboard, click SETUP, then SCAN.
  3. Click BIKE next to the device whose name starts with ECH.
  4. Type in your weight, FTP and max heart rate so the numbers are calculated for you.

Done. Your bike is remembered and reconnects by itself next time.

No bike yet? Try the demo

Hold Shift, right-click the folder your BikeStation.exe is in, choose Open PowerShell window here, and run:

.\BikeStation.exe --demo

That runs the full dashboard on simulated ride data, with no bike and no Bluetooth.

Compatibility

Echelon Connect bikes share one proprietary GATT protocol, and BikeStation targets the protocol rather than any single model. Nothing about the bike is hardcoded: devices are matched on the ECH… advertising prefix, and the resistance range is queried from the bike (F0 A3) rather than assumed, so a bike reporting 1–24 works the same as one reporting 1–32.

Verified end to end on a Connect EX-5. Other Connect models — Sport, EX-3, EX-5s and relatives — advertise the same service and frame format and are expected to work, but I have only one bike to test against. Reports welcome either way.

Two things genuinely vary between models, and both degrade gracefully:

  • Resistance writing. Reading telemetry works on any bike that speaks the protocol. Setting resistance (F0 B1) needs motorised resistance to act on — that drives the slider, ERG mode, structured workouts and knob sensitivity. On a bike whose knob is purely mechanical the metrics still work and those controls simply will not move it.
  • Power figures. No Echelon bike has a power meter, so watts are estimated from a resistance/cadence table calibrated on a Connect Sport. Expect the numbers to be self-consistent rather than absolutely accurate, and more approximate the further your model sits from that calibration. Two tables ship; switch them in SETUP.

Recent firmware on some bikes refuses to stream to third-party apps until the official Echelon app has unlocked it (frame F0 E0); see Troubleshooting.

The protocol was worked out by the community, not by me. BikeStation builds on qdomyos-zwift, the echelon-bike firmware clone, echbt and SmartSpin2k — see Licence.

Run from source

Only needed if you want to change the code — the download above is self-contained. Requires Python 3.10 or newer (both bleak and aiohttp need it).

BikeStation.bat

That installs bleak + aiohttp on first launch, starts the server and opens http://127.0.0.1:8730/. Useful flags:

command what it does
BikeStation.bat normal ride mode
BikeStation.bat --demo synthetic bike firmware — full UI, no Bluetooth
BikeStation.bat --scan list BLE devices and exit (find your bike's address)
BikeStation.bat --port 9000 different HTTP port
BikeStation.bat --no-browser don't open a browser

Connecting the bike is the same as for the executable — see step 5 above. If you own a Bluetooth HR strap, click STRAP next to it in SETUP: it is more accurate than the bike's grip sensors and also gives RR intervals.

Build a standalone executable

Users without Python can run a single bundled BikeStation.exe:

pip install pyinstaller
python -m PyInstaller bikestation.spec --noconfirm --clean

The result lands in dist/BikeStation.exe (~12 MB, no Python install needed). Two details the spec exists to handle:

  • the dashboard's HTML/CSS/JS/fonts are read at runtime, so bikestation/web is bundled and resolved through bikestation/paths.py rather than assumed to sit next to the source;
  • bleak reaches Windows Bluetooth through dynamically imported winrt.* modules, which static analysis cannot see, so they are collected explicitly.

Where a frozen build keeps your data

The executable is a single portable file that people move, copy to another machine, or drop somewhere unwritable such as Program Files. So a frozen build does not keep data beside the .exe; it uses a fixed per-user directory instead:

%LOCALAPPDATA%\BikeStation        e.g. C:\Users\<you>\AppData\Local\BikeStation

Move or rename the executable as much as you like — it always finds the same rides, settings and class library. %TEMP% is deliberately not used, because Storage Sense and Disk Cleanup empty it and your ride history would disappear. Running from a source checkout still uses data/ inside the repo, which keeps a clone self-contained.

--data-dir <path> overrides the choice either way, and DIAGNOSTICS shows the resolved location as data folder so you can always find your rides.

Metrics

Read from the bike (it only transmits these four things):

  • cadence (rpm, 16-bit)
  • cumulative crank revolutions
  • resistance level and the bike's real resistance range (queried from the bike, commonly 1–32)
  • heart rate, when a strap is paired to the bike itself
  • plus model id / hardware / firmware version from the device-info frame

Derived locally, at 1 Hz:

  • power — the bike has no power meter; watts come from the calibrated 33 × 11 resistance/cadence table (switchable default / mgarcea in SETUP)
  • speed (console-matching 0.375 km/h per rpm, or a road-load model from power), distance, kJ of work, kcal
  • average and maximum power, cadence, speed, resistance, heart rate
  • normalised power, intensity factor, TSS, W/kg
  • time in each of the 7 power zones and 5 heart-rate zones
  • Peloton-equivalent resistance
  • laps, auto-pause, and a per-second raw log

The RIDE tab is split in two so everything fits on one screen without scrolling: GAUGES (watts / rpm / resistance rings, session clock, the live strip and the control panel) and TELEMETRY (power analytics, the chart, zone breakdown and laps). The chosen sub-tab is remembered between sessions.

The TELEMETRY sub-tab: power, cadence, heart-rate and resistance traces above the power and heart-rate zone breakdown and the lap table

Controls

  • resistance −/+ and slider (writes F0 B1 to the bike)
  • Knob sensitivity (SETUP) — the bike's knob moves one resistance level per quarter turn, so covering the full 1–32 range takes roughly eight turns. Set a multiplier of 1–10× and BikeStation watches the F0 D2 level reports, multiplies each knob movement, and writes the result back, so fewer rotations cover the range. 1× leaves the knob untouched. Higher values are coarser: at 4× a quarter turn moves 4 levels. Requires a bike that accepts resistance writes.
  • ERG mode — pick a wattage; resistance is re-trimmed every 2 s as your cadence drifts so the power target holds
  • Workouts — structured interval sessions from data/workouts/*.json (warm-up, sweet spot 3×12, VO2max 5×4, 20-min FTP test, endurance). Segments are absolute watts, % FTP, or a fixed resistance level; each segment change starts a lap.
  • bike control-state frames (stream on / pause / stop)
  • Keep awake (SETUP) — Windows sleeps on input idleness, and pedalling is not input, so a ride would otherwise be cut off mid-class. While enabled BikeStation holds off both system and display sleep (SetThreadExecutionState) and releases it on exit. On by default; untick it in SETUP to leave the normal power timers alone. DIAGNOSTICS shows the live state as keep awake. Windows only — a no-op elsewhere, with a warning in the log.
  • keyboard: / resistance, / gauges / telemetry, Space pause, L lap, S start, F focus mode, 1/2/3 manual / erg / workout

Spin class

The CLASS tab embeds a YouTube class next to a live metric rail, so one window covers both the video and the ride. Paste a link (watch, youtu.be, /live/, /shorts/, or a bare video id — ?t= timestamps are honoured) and hit LOAD. HIDE METRICS gives the video the full width.

The CLASS tab: a saved YouTube playlist expanded in the library, with the live metric rail beside the video area

LIBRARY holds classes and whole playlists, stored server-side in data/classes.json so they survive a browser change and land in your backups next to rides and settings. Paste a playlist link and press ADD TO LIBRARY and every video in it is resolved with titles and durations — no Google API key needed. Playlists expand and collapse, re-fetches one after the channel adds classes, and × removes a playlist or a pinned class. A single video link pins just that class.

Chapter laps. Many structured classes ship YouTube chapters (warm-up / intervals / cool down). When a class loads, its chapters are fetched and the current one shows in the rail; crossing into a new chapter starts a lap, so the lap table lines up with the class segments. Arriving into the chapter that is already playing does not start a lap, and neither does re-crossing one you already passed. Videos without chapters simply do nothing. Turn it off with the chapter laps checkbox.

With sync ride on, pressing play starts or resumes the ride and pausing the video pauses it — the timer, recording, and laps follow the class instead of needing separate clicks. The rail carries power, cadence, resistance, heart rate, the session clock, the current chapter, a resistance slider, a rolling power sparkline with an FTP reference line, and START/PAUSE/LAP.

Playback uses a plain embed iframe and deliberately does not depend on YouTube's iframe_api script, which ad blockers routinely filter — if that script is blocked you lose ride sync, not the video, and a status line says so. DIAGNOSE probes www.youtube.com, i.ytimg.com and the API script and reports which of them this browser can actually reach, which separates an extension or DNS blocker from a genuine network problem.

Videos whose uploader disables embedding cannot play here — BikeStation says so and offers a link to open it on YouTube. Note that while the video has keyboard focus, YouTube swallows key presses, so use the on-screen −/+ or slider for resistance rather than the arrow keys.

Files

The data root is data/ in a source checkout, or %LOCALAPPDATA%\BikeStation for the executable — DIAGNOSTICS shows the resolved path as data folder.

settings.json          rider profile + last device
classes.json           pinned classes + saved YouTube playlists
workouts/*.json        editable interval workouts
rides/ride-*.csv       every second of the ride
rides/ride-*.tcx       import into Strava / Garmin / intervals.icu / TrainingPeaks
rides/ride-*.json      summary incl. laps and zone distribution

A ride starts automatically when you begin pedalling and auto-pauses when you stop; FINISH writes the files. HISTORY lists past rides with download links.

Troubleshooting

Nothing found when scanning. Pedal once to wake the bike. Close the Echelon phone app completely — the bike accepts a single connection at a time. Do not pair the bike in Windows Bluetooth settings; BikeStation connects without pairing.

Connects but all metrics stay 0 / "BIKE LOCKED". Recent Echelon firmware refuses to stream to third-party apps until the official app has unlocked the bike (frame F0 E0, see qdomyos-zwift issues #4750 and #4868). Open the Echelon app once, start and end a free ride, force-close the app, then reconnect here.

Link drops mid-ride. BikeStation reconnects automatically with backoff, replays the handshake and re-applies your resistance level (the bike forgets it on disconnect). If it keeps failing, power-cycle the bike.

Wattage looks wrong. It is a table estimate, not a measurement. Try the mgarcea table in SETUP, which reads lower at high resistance.

Diagnostics tab shows the raw hex frames, per-opcode counters, checksum errors, the full GATT tree and the event log — that is the first place to look if anything misbehaves.

Protocol notes

Service 0bf669f1-45f2-11e7-9598-0800200c9a66; write …f2, notify …f3 (command responses) and …f4 (telemetry). Frames are F0 | opcode | length | payload | checksum, checksum = low byte of the sum of the preceding bytes.

direction frame meaning
F0 A1 00 91 get device info (model, hw, fw)
F0 A3 00 93 get resistance range (reply max,min)
F0 A5 00 95 get resistance level
F0 B0 01 01 A2 control state: 1 = start streaming, 2 = pause, 0 = stop
F0 B1 01 <lvl> set resistance
F0 A0 01 <ctr> keepalive, sent every 2 s
F0 D1 09 … telemetry: [3:5] timestamp, [5:9] revolutions, [9:11] rpm, [11] heart rate
F0 D2 01 <lvl> resistance changed (only sent on change)
F0 E0 … bike locked

Note that qdomyos-zwift decodes [7:9] of the telemetry frame as distance and reads cadence from byte 10 only; per the bike-firmware clone those bytes are the low half of the 32-bit revolution counter and the low byte of a 16-bit rpm. BikeStation uses the firmware layout, which is also why it can surface heart rate and revolutions.

Disclaimer

Not affiliated with, endorsed by, or supported by Echelon Fitness. "Echelon" and product names are trademarks of their respective owners. This project talks to the bike over an undocumented protocol worked out by the community; it can stop working whenever Echelon ships new firmware, and using it may affect your warranty or your terms of service with Echelon. It is provided with no warranty of any kind — see the licence.

It also writes resistance changes to a machine you are pedalling. Resistance can change under your legs with no warning, particularly in ERG mode, during a structured workout, or with knob sensitivity above 1×. Do not use it unattended, and stop pedalling before experimenting with the controls.

Ride data stays on your machine. Nothing is uploaded anywhere, and there is no telemetry. data/ is git-ignored because it holds your rider profile and every ride you have recorded.

Licence

Copyright (C) 2026 Nathan Brittin.

GPL-3.0-or-later. The full text is in LICENSE.

That choice is deliberate rather than a default. The Echelon protocol here was worked out by reading prior community reverse-engineering, and the strictest of those projects is GPL-3.0, so releasing under the same terms is safe whether this counts as an independent reimplementation or a derivative work:

project licence
qdomyos-zwift GPL-3.0
SmartSpin2k GPL-2.0
echelon-bike no licence file — all rights reserved by default
echbt verify before reusing anything from it

Frame layouts and UUIDs are functional facts rather than creative expression, so documenting them is not itself derivation — but no code should be copied out of echelon-bike, which carries no licence and is therefore all rights reserved.

If you distribute BikeStation.exe, GPL-3.0 obliges you to make the corresponding source available to whoever receives it. Attaching the binary to a GitHub release of this public repository satisfies that, because the matching source sits in the same place. Bundled dependencies are compatible: bleak (MIT), aiohttp (Apache-2.0), the winrt projections (MIT) and CPython itself (PSF) may all be combined with GPL-3.0 code.

About

Local Windows dashboard for the Echelon Connect EX-5 over Bluetooth LE - live metrics, resistance and ERG control, ride recording to CSV/TCX, and a YouTube spin-class tab. No Zwift, no subscription, no cloud.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages