Collection of scripts for BEStie ride data quality analysis
BEStie acts as a protocol translator between Bosch LDI and Bluetooth FTMS. This adds an extra radio hop between Bosch ebike and fitness data recording device (cycling computer, watch, etc.). Bluetooth Low Energy works in discrete connection windows which for multi hop solution introduces jitter.
As a result existing data quality analysis tools such as Compare the watts or Quantified Self do not know how to align ground truth file from Bosch ebike with ride recorded using BEStie and falsely indicate bad data quality (<80% correlation). This is result of strict assumption that there's no jitter and each analyzed file is coming from a single hop system.
We needed a better solution.
fit_align.py on the other hand knows exactly how FTMS message is constructed. It scans for random jitter within set time window for ech power sample and applies that to power and cadence data. This reflects power and cadence being updated atomically. Separate jitter scan is done for speed data because speed is sent in a separate time interval and acts as 'no more data' packet for power and cadence.
As a result typical correlation is around 99%. Remaining 1% accounts for situations where multiple samples arrive within a single 1s sampling window (.fit file limitation).
This project's code was generated by Deepseek v4 fed with statistical knowledge primer. Results were verified by adversary expert agent, comparing against manually aligned data set and artificial data runs.
python3 -m venv ./.venv
./.venv/bin/pip3 install -r requirements.txt
. ./.venv/bin/activateUsage: ./fit_align.py --output-dir out_fit --global-shift 1 --jitter 2 bosch.fit bestie.fit
This will compare ground truth data from Bosch Flow app in bosch.fit against a ride recorded via BEStie in bestie.fit and store output in out_fit/ folder. BEStie data will have a global shift of 1s and alignment will be done within 2s jitter window.
Resulting output looks like:
Reading Bosch .fit: bosch.fit
Reading comparison .fit: bestie.fit
Bosch records: 10582 raw rows
Bosch CSV: out_fit/bosch_converted.csv (10582 rows)
Comparison CSV: out_fit/comparison_converted.csv (6252 rows)
Bosch collapsed: 5566 unique timestamps
Comparison: 6252 rows (2026-07-03 13:05:04+00:00 .. 2026-07-03 15:04:04+00:00)
Global shift: +1s (manual)
Running per-sample jitter correction (power: ±2s, speed: ±2s) ...
============================================================
FIT ALIGN — Correlation Summary
============================================================
Global shift: +1s (mean ρ = 0.0000)
Matched samples: 5566
Param ρ MAE Samples
────────────────────────────────────
Power 0.9956 0.3W 3930
Cadence 0.9879 0.2rpm 1787
Speed 0.9936 0.3m/s 5045
============================================================
Metric |Δ|≤1 1<|Δ|≤2 2<|Δ|≤3 |Δ|>3
─────────────────────────────────────────────────────────────────
Power 3909 ( 99.2%) 33 ( 0.8%) 0 ( 0.0%) 0 ( 0.0%)
Cadence 1774 ( 98.3%) 30 ( 1.7%) 0 ( 0.0%) 0 ( 0.0%)
Speed 4553 ( 90.2%) 492 ( 9.8%) 0 ( 0.0%) 0 ( 0.0%)
============================================================
Summary: out_fit/correlation_summary.txt
Matched CSV: out_fit/matched_output.csv (5566 rows)
Saved: alignment_zoom.png
Saved: alignment_full.png
Done. Output directory: out_fit
most important part is correlation table:
Param ρ MAE Samples
────────────────────────────────────
Power 0.9956 0.3W 3930
Cadence 0.9879 0.2rpm 1787
Speed 0.9936 0.3m/s 5045
showing 99.56% correlation for power, 98.79% for cadence and 99.36% for speed. Values are also translated into error estimates (0.3W, 0.2RPM, 0.3m/s)
Artifacts in output folder:
correlation_summary.txt- correlation data tablesbosch_converted.csvandcomparison_converted.csv- input .fit data converted to CSVmatched_output.csvaligned data setalignment_zoom.pngandalignment_full.pngcharts showing both data sets on top of one another