KayakFit is a desktop app for logging KayakFirst Bull ergometer workouts. It
connects over Bluetooth to your ergometer and, optionally, a standard BLE
heart-rate monitor. It shows a live metric dashboard while you paddle and saves
each session as a .csv and a .fit file, with one-click upload to Strava.
Runs on macOS and Windows.
- Live dashboard — time, distance, instantaneous speed, pace, stroke rate, pull force and estimated power, plus heart rate, updated once per second.
- Heart‑rate zones — colored zones with bpm ranges, derived from your max HR or set manually.
- Auto‑pause & laps — the live pause state and free-lap boundary require five continuous seconds without movement; each meaningful effort becomes a lap in the FIT file. When you run a structured program, the FIT laps follow the program steps instead, so each interval (warm‑up, work, rest…) shows up as its own lap on Strava.
- Structured training programs — run interval sessions (e.g. 4 × 500 m / 1:00 rest) with a live step panel, countdowns, and audible cues on each phase change.
- Workout summary — per‑workout detail screen with stacked charts for speed, power, heart rate, cadence and pull force on one shared time axis (pick the metrics you want, hover for exact values), time in HR zones, and per‑lap splits.
- Estimated power — the ergometer has no power sensor; KayakFit applies its power model to each instantaneous force/cadence point.
- Strava upload — convert to FIT and upload automatically or on demand. FIT records map active ergometer distance onto a fixed Hazewinkel course so Strava can infer pauses from stationary positions in its overview.
- Robustness — crash recovery for interrupted sessions, inactivity auto‑stop, the screen is kept awake during a workout, the HRM battery level is shown so a dying strap isn’t a surprise, and the Stop button stays responsive even while the ergometer connection is retrying.
- First‑run setup wizard to get your devices connected quickly.
Hardware
- A KayakFirst Bull ergometer (Bluetooth).
- (Optional) Any standard BLE heart‑rate monitor.
- A Mac or Windows PC with Bluetooth.
Software (to run from source or build)
- uv — the dependency manager and runner.
- Python 3.14. You don't
need to install Python yourself:
uvfetches the right version for you.
You don’t need Python or uv at all if you use a packaged build (see Building a standalone app).
# 1. Clone the repo and enter it
cd KayakFit
# 2. Install dependencies into a managed .venv (creates it, fetches Python 3.14
# if needed, and installs exactly what uv.lock pins)
uv sync
# 3. Run the app
uv run python kayakfit_gui.pyuv run executes inside the project environment without activating it; if you
prefer, source .venv/bin/activate (macOS/Linux) or .venv\Scripts\activate
(Windows) once, then run python kayakfit_gui.py directly.
No uv? A requirements.txt is generated from the lock as a pip fallback:
python3 -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt.
On macOS you’ll be asked to grant Bluetooth permission the first time.
On first launch a short setup wizard appears:
- Enter the device name printed in large letters on the ergometer, then scan for Bluetooth devices (turn your ergometer on and keep it nearby).
- Pick your ergometer and, optionally, a heart‑rate monitor.
- Optionally choose to set up Strava afterward.
You can redo any of this later in Settings.
- Create an API application at https://developers.strava.com.
- Set its Authorization Callback Domain to
127.0.0.1. - In KayakFit → Settings → Strava, paste your Client ID and Client Secret and click Connect to Strava. A browser window opens for you to authorize; KayakFit captures the result automatically.
Your complete Strava authorization is stored in the operating-system keychain,
not in a plain file. It survives deletion of ~/KayakFit; use Disconnect in
Settings to remove it from the keychain.
- Set Boat (kg) and Person (kg) in the setup bar for the ergometer's workout calculations.
- (Optional) Pick a Program from the dropdown, or leave it on Free workout.
- Click Start workout (or press the spacebar).
- Paddle. The dashboard updates live; the heart‑rate tile is tinted by zone.
- When you’re done, click Stop & save — you’ll be asked to tap again to confirm so a stray click can’t end the session. Press the spacebar twice for the same effect.
On stop, the workout is saved as CSV + FIT and (if enabled) uploaded to Strava.
During the workout you’ll also see:
- a ⏸ Paused badge and dimmed tiles after five seconds of continuous inactivity,
- a Lap N counter,
- the program panel (if running a program) with the current step, a countdown to the next phase, the next step, and two progress bars — one for the current step and a thin one for the whole workout,
- warning banners for things like a lost connection or a low HRM battery.
If you forget to stop, the workout auto‑stops and saves after 30 minutes of no movement.
Workouts is the one place for past sessions. It lists them newest first; use the search box to filter the loaded list by filename or workout type, click one to select it (the most recent is pre‑selected, double‑click opens its summary), and use the action bar at the bottom: Summary and Save FIT (CSV files), and Upload (CSV or FIT, needs a connected Strava account). Open folder reveals the files in Finder/Explorer, and Choose file… adds a workout stored somewhere else to the list so the same buttons work on it.
Summary opens a detail screen for a workout: headline stats (active distance, active/elapsed/pause time, average/max sensor speed, heart rate, power, stroke rate), stacked per‑metric raw-point charts — sensor speed, power, heart rate, cadence and pull force, each with its own value axis on one shared time axis — time in each heart‑rate zone, and a per‑lap splits table. Checkboxes above the chart choose which metrics are shown, and hovering shows a crosshair with the exact values at that moment. Heart‑rate elements appear whenever the workout has HR data. The laps match the FIT export, so Strava receives the same split structure (although Strava controls how it presents imported laps). The same screen is available from the View summary button on the "Workout saved" card right after you stop a workout.
For the exact rules and worked examples, see
docs/DATA_TRANSFORMATIONS.md. It explains
active versus elapsed time, pause handling, speed and power calculations,
free/planned laps, FIT fields, the synthetic GPS track, and what Strava may
recalculate after upload.
A program is a sequence of steps (warm‑up, work, rest, cool‑down). All plans
live as JSON files in ~/KayakFit/programs/ and appear in the Program
dropdown (refreshed when the window regains focus). On first run the bundled
example plans are copied there, so you can edit, rename, or delete them like
any other plan — deleted examples stay deleted. The 📂 button next to the
dropdown opens the folder.
To add your own, drop a .json file like this into ~/KayakFit/programs/:
{
"name": "5 × 400 m in Zone 4 / 90 s rest",
"steps": [
{"type": "warmup", "duration": {"kind": "time", "seconds": 300}},
{"repeat": 5, "steps": [
{"type": "work",
"duration": {"kind": "distance", "meters": 400},
"target": {"metric": "hr_zone", "low": 4, "high": 4}},
{"type": "rest", "duration": {"kind": "time", "seconds": 90}}
]},
{"type": "cooldown", "duration": {"kind": "time", "seconds": 300}}
]
}| Field | Values |
|---|---|
type |
warmup · work · effort · rest · cooldown |
duration.kind |
time (with seconds), distance (with meters), or open (advance manually) — the step advances when that time/distance is reached, or immediately when you press Next step for an open step |
label |
(optional) overrides the auto‑generated step name (e.g. "2 km TT") |
target |
(optional) the intended intensity for the step — see below |
repeat |
wraps a list of steps and repeats them a positive whole-number of times |
A step may carry a target of the form {"metric", "low", "high"} describing
the effort you’re aiming for. low/high are inclusive bounds — use the same
value for both to target a single zone.
metric |
What low / high mean |
Example |
|---|---|---|
hr_zone |
heart‑rate zone 1–5 | {"metric": "hr_zone", "low": 3, "high": 3} → hold Zone 3 |
pace_200 / pace_500 / pace_1000 |
seconds per that split | {"metric": "pace_500", "low": 130, "high": 145} → 2:10–2:25 per 500 m |
power |
watts | {"metric": "power", "low": 180, "high": 220} |
spm |
strokes per minute | {"metric": "spm", "low": 58, "high": 64} |
Heart‑rate zones follow your Settings → Heart‑rate zones (auto from Max HR,
or your manual bpm bounds), so hr_zone targets adapt to your configuration.
While a step with a target is running, the program panel shows a live badge —
On target (green), Too hard ↑ (red) or Too easy ↓ (amber) — comparing
your current effort to the target range, and a sound plays when you drift out of
range (if sound cues are enabled). Steps without a target (like a plain 500 m
interval) simply omit the badge.
To avoid flicker from normal second‑to‑second fluctuation, the badge only changes once a status has held for a short grace period (default 3 s, set via Settings → Logging & processing → Target grace) — so a brief blip across the boundary won’t flip the color or trigger the alert.
The program panel has a Next step ▸ button (also the → arrow key) that
advances to the next step — required for open steps, and handy to skip ahead on
timed/distance steps. When a program finishes, the workout stops and saves
automatically.
When you record with a program, KayakFit remembers each step's boundaries and uses them to define the laps in the exported FIT file — so a 4 × 500 m session appears on Strava as clean interval laps rather than laps guessed from auto‑pauses. (Free workouts still fall back to auto‑pause laps.)
| Setting | What it does |
|---|---|
| Devices | Scan for, select and test your ergometer / HRM. A Use heart‑rate monitor switch disables the HRM entirely — no connection attempt and no "continue without HRM?" prompt if you don't own a strap. |
| Weight defaults | Default boat and person weight, and Max HR. |
| Heart‑rate zones | auto (derive zones from Max HR) or manual (set the five bpm bounds). |
| Display layout | Which metrics the ergometer’s own screen shows. |
| Logging & processing | Log interval/level, auto-upload, sound cues on program phase changes, and the target grace (seconds before the target badge changes). |
| Strava | Connect/disconnect your Strava account. |
Everything lives under a KayakFit folder in your home directory
(~/KayakFit on macOS, C:\Users\<you>\KayakFit on Windows):
KayakFit/
├── workouts/<year>/workout_YYYYMMDD_HHMMSS/
│ ├── workout.csv # raw sensor samples
│ ├── metadata.json # recorded processing settings
│ ├── steps.json # planned workouts only
│ └── activity.fit # created on save/upload
├── programs/*.json # training plans (examples + your own)
├── config.yml # settings
└── logs/kayakfit.log # rotating diagnostic log
The complete Strava authorization is kept in the OS keychain, not in
config.yml. It survives deletion of this folder; use Disconnect in
Settings to remove it.
One tuning value worth knowing about: pull_length_m in config.yml is the
effective handle pull length used by the power model (default 0.600, applied
to instantaneous force and cadence). If your machine or firmware reads differently,
it can be refitted — the procedure is described in
docs/POWER_MODEL.md.
Builds use PyInstaller with the cross‑platform KayakFit.spec. Run the build
on the OS you’re targeting (build on a Mac for the Mac app, on Windows for the
Windows app).
PyInstaller lives in the build dependency-group, so install it first with
uv sync --group build, then run the build with uv run.
macOS
uv sync --group build
./pyinstaller_build.sh # or: uv run pyinstaller KayakFit.spec --distpath dist-mac --workpath build-mac
# Result: dist-mac/KayakFit.appWindows
uv sync --group build
pyinstaller_build.bat :: or: uv run pyinstaller KayakFit.spec --distpath dist-win --workpath build-win
:: Result: dist-win\KayakFit.exe (single-file executable)Each platform builds into its own dist-mac/dist-win (and build-mac/build-win)
folder. This matters if the project directory is shared between a Mac and a Windows
machine (e.g. a mapped network drive): a macOS .app bundle contains Unix symlinks
that Windows can't delete, so a shared dist//build/ folder can break the other
platform's clean step.
The spec bundles the app icon, the presets/ training plans, and the required
data files for bleak, customtkinter, fit‑tool and keyring. (Strava is reached
through a small requests-based client, so no third-party Strava library is
bundled.)
The tests are plain-assert pytest tests:
uv run pytestOr run a single suite while working on it:
uv run pytest tests/test_segmentation.py # auto‑pause / lap detectionThe suites cover: test_pipeline (CSV → FIT conversion + config validation),
test_program (training‑program engine), test_program_laps (program‑step →
lap reconciliation), test_segmentation (auto‑pause / lap detection),
test_fit_events (FIT start/stop event timeline), test_read_csv (CSV reader),
test_hr_zones (heart‑rate zones), test_recovery (crash‑recovery marker),
test_ble_parsing (ergometer packet framing), test_summary (workout summary,
incl. FIT/summary endpoint agreement), test_stats (shared avg/max metric
aggregation) and test_strava (Strava REST client).
Lint and type checks. Contributions to main must also pass ruff
(lint + import order) and mypy (static types) with zero errors — CI enforces
both, so run them before opening a pull request (both are in the dev group
installed by uv sync):
uv run ruff check .
uv run mypy . --strictSee CONTRIBUTING.md for the full workflow (branching, PRs, releases).
Repository documentation is split by purpose:
docs/AGENTS.md— instructions for AI assistants.docs/ARCHITECTURE.md— runtime and data flow.docs/DATA_TRANSFORMATIONS.md— sensor-to-summary, FIT, and Strava calculations with worked examples.docs/FEATURES.md— implemented user-facing features.docs/ROADMAP.md— prioritized open findings.docs/DECISIONS.md— architecture decisions and invariants.docs/STYLE_GUIDE.md— coding and test conventions.
Project layout
app/ device I/O (BLE), CSV/FIT, Strava, programs, session logic
gui/ the customtkinter interface
tests/ unit/integration tests
presets/ example training programs (JSON), copied to ~/KayakFit/programs on first run
assets/ icons
docs/ assistant instructions, architecture, features, roadmap, decisions, and style
kayakfit_gui.py application entry point
KayakFit.spec PyInstaller build spec
- No devices found when scanning — check that the ergometer device name is entered exactly as printed on the machine, make sure Bluetooth is on, the ergometer is powered and nearby, and (macOS) that KayakFit has Bluetooth permission. Use Settings → Test connection to verify a device is reachable.
- Workout won’t start / HRM not found — if a configured strap can’t be reached you’ll be asked whether to continue without heart rate. If you don’t use a strap at all, turn off Use heart‑rate monitor in Settings → Devices and the question goes away for good.
- Strava upload fails — check that your API app’s callback domain is
127.0.0.1, and that KayakFit shows Strava · connected in the status bar. - A workout was interrupted — on the next launch KayakFit offers to recover and export the unfinished session; the CSV is always preserved in Workouts.