DIY speed, cadence & heart-rate dashboard for the NordicTrack Commercial 2450 β feeds a Garmin Fenix 8 with speed, cadence and grade over BLE, adds live BLE heart rate, ToF-based incline sensing, auto-pause, virtual GPS routes and one-tap Strava upload.
The in-run cockpit on the 800Γ480 kiosk β distance, time, HR zone, pace, live cadence and grade. (Rendered mock-up of the actual UI.)
| Feature | Details |
|---|---|
| Live Speed Tracking | DRV5023 Hall-effect sensor (separate PCB) β Feather nRF52840 β Pi via USB serial |
| Real Cadence | Footfall cadence detected on the Pi from the frame accelerometer (band-pass + windowed autocorrelation), no foot pod β validated Β±4 spm from walk to run, pushed to the Fenix over BLE |
| Incline / Grade | VL53L4CD time-of-flight sensor reads deck tilt; 6-point calibration maps it to % grade, mirrored to the Fenix so watch grade matches the dashboard |
| Heart Rate | Garmin HRM-Pro Plus chest strap β Pi via BLE (bleak) + Fenix 8 via ANT+ simultaneously; Karvonen HR zones |
| Garmin Fenix 8 Integration | Feather broadcasts speed + cadence + grade over BLE to the Fenix 8; HRM-Pro Plus sends HR over ANT+ |
| Web Dashboard | Python HTTP server + React/JSX kiosk UI on a HyperPixel touchscreen, mounted on the treadmill arm |
| Virtual Routes | Fixed 400 m athletics track or a random pick from your GPX pool (anti-repeat) for nicer Strava maps |
| Auto-Pause | Detects belt stop β yellow "AUTO-PAUSE" state |
| Strava Upload | One-tap SAVE β export .tcx/.fit and auto-upload via the Strava API, with an offline retry queue |
| Custom PCBs | Hall sensor PCB (60 mm, JLCPCB) + main sensor PCB (Feather + LSM303DLHC + VL53L4CD) |
| 3D-Printable Enclosures | OpenSCAD: Pi + HyperPixel arm clamp & Feather sensor mount |
Over BLE the Feather sends the Fenix 8 speed, cadence and grade. The Pi computes cadence (from the frame accelerometer) and grade (from the ToF sensor) and pushes them to the Feather, which relays them to the watch; the Feather keeps its own onboard estimates as a fallback if the Pi push goes stale.
See the full Bill of Materials for part numbers and sourcing.
- Raspberry Pi (3B+ or 4) β dashboard host (
EddyPi), receives speed data via USB serial, HR via BLE - Adafruit Feather nRF52840 β sensor MCU on main PCB, running S340 SoftDevice (BLE + ANT+), communicates with Pi over USB serial and broadcasts speed to Garmin Fenix 8 over BLE
- LSM303DLHC β accelerometer/magnetometer on the main PCB; the accelerometer doubles as the cadence pickup (footfall vibration through the frame)
- VL53L4CD β time-of-flight distance sensor on the main PCB, reads deck tilt for incline / grade (6-point calibration β % grade)
- DRV5023 Hall-Effect Sensor β on a separate 60 mm PCB, wired to the Feather, detects belt magnets
- HyperPixel Display β SPI touchscreen dashboard, mounted on treadmill arm
- Garmin HRM-Pro Plus β sends HR to Pi via BLE and to Garmin Fenix 8 via ANT+ simultaneously
- Garmin Fenix 8 β receives speed from Feather (BLE) and HR from HRM-Pro Plus (ANT+)
Hall Sensor PCB (60 mm) β designed in EasyEDA Pro, manufactured by JLCPCB:
- ENIG surface finish
- SMT assembly (DRV5023 bottom-side, Standard PCBA tier)
- J1 screw terminal for wiring to main PCB
- Status LED: NationStar NCD0603C1 (0603, yellow-green)
Main Sensor PCB β carries the Feather nRF52840 + LSM303DLHC + VL53L4CD:
- Connects to Hall sensor PCB via wiring
- USB connection to Raspberry Pi for serial data
- Designed in EasyEDA Pro
β³ Planned β not yet in this repo (the OpenSCAD sources are still WIP; see Roadmap).
| Enclosure | Description |
|---|---|
pi-hyperpixel-clamp.scad |
Clamps Pi + HyperPixel to treadmill arm |
feather-sensor-mount.scad |
30 mm max profile, strap/zip-tie mount for sensor PCB |
- Raspberry Pi running Raspberry Pi OS (Bookworm or later)
- Python 3.9+
- Feather nRF52840 with S340 SoftDevice flashed
- Strava API application (for upload feature)
git clone https://github.com/eddyCG42/treadmill-sensor.git
cd treadmill-sensorpip install -r dashboard/requirements.txtCreate ~/strava_config.json on your Pi:
{
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET"
}
β οΈ Never commit your Strava tokens. They are excluded via.gitignore.
The dashboard connects via BLE using bleak. Set your strap's MAC as hr_mac
in ~/treadmill_config.json (with hr_max / hr_rest for the Karvonen zones).
cd dashboard/web_dashboard
bash deploy.sh # installs the systemd services, fetches vendored React/Babel, starts everythingdeploy.sh generates and enables the treadmill-server (system) and
treadmill-kiosk (user) units. See dashboard/DEPLOY_v12.md
for the full coupled firmware + Pi deployment guide.
See Firmware Notes below and the Adafruit nRF52 Bootloader S340 PR for Arduino IDE build instructions and S340 SoftDevice setup.
treadmill-sensor/
βββ README.md
βββ LICENSE
βββ .gitignore / .gitattributes
βββ docs/
β βββ BOM.md β Bill of materials
βββ firmware/
β βββ feather-nrf52840/
β βββ treadmill_sensor_v11_21.ino β Arduino sketch (internal v12.1)
βββ dashboard/
β βββ treadmill_cadence.py β Pi-side footfall / cadence detector
β βββ treadmill_metrics.py β shared speed / incline / elevation math
β βββ treadmill_routes.py β GPX virtual routes (random + fixed track)
β βββ treadmill_export.py β TCX / FIT activity export
β βββ treadmill_hr.py β BLE heart-rate monitor
β βββ treadmill_strava.py β Strava OAuth + upload
β βββ treadmill_strava_queue.py β offline upload queue
β βββ cadence_calibrate.py β offline cadence-calibration tool
β βββ requirements.txt
β βββ DEPLOY_v12.md / RECORD_CADENCE.md
β βββ web_dashboard/
β βββ treadmill_server.py β HTTP server + run state machine
β βββ deploy.sh / setup.sh
β βββ static/ β dashboard.html + *.jsx (React, in-browser Babel)
β βββ kiosk/ β systemd units + Chromium kiosk launcher
βββ pcb/
β βββ easyeda/ β Gerber exports, schematic PDF
βββ strava_config.json.example β Template (no secrets)
| Home / mode select | Cadence calibration |
|---|---|
![]() |
![]() |
Left: pick the fixed 400 m track or a random GPX route, with a live sensor check before starting. Right: the CADENCE tab β the "VERROUILLAGE DΓTECTEUR" bar shows the Pi footfall detector locking onto your rhythm (green = locked).
| Speed calibration | Incline calibration (6 points) |
|---|---|
![]() |
![]() |
Speed: hold the belt at a known km/h and tap CALIBRER 10s to learn the pace factor. Incline: capture the raw ToF reading at β3/0/3/6/9/12 % for a piecewise grade map (the firmware mirrors the 0 and 12 % points as a fallback).
End-of-run activity summary (review, then save + auto-upload to Strava):
The web dashboard (dashboard/web_dashboard/treadmill_server.py, a lightweight
Python HTTP server + React/JSX kiosk UI) provides:
- Live speed from the Hall-effect sensor (via Feather β USB serial)
- Real cadence β band-pass + windowed-autocorrelation footfall detection on the Pi from the frame accelerometer, pushed to Garmin over BLE (validated Β±4 spm)
- Heart rate from HRM-Pro Plus (BLE via
bleak) with Karvonen HR zones - Incline β 6-point ToF calibration, mirrored to Garmin so grade matches
- Distance & duration tracking, elevation from live grade
- Auto-pause with delay and yellow visual indicator
- Virtual routes β fixed athletics track or a random pick from your GPX pool
- SAVE button β exports
.tcx/.fitand auto-uploads to Strava (offline queue)
NordicTrack Commercial 2450 (NTL19124)
- Uses sensorless speed control via MC1648DLS (back-EMF estimation)
- The displayed speed is a commanded setpoint, not a direct measurement
- Accuracy is approximately Β±2.3%
- This project adds independent speed measurement via belt-mounted magnets and a Hall-effect sensor
The Feather nRF52840 runs the S340 SoftDevice (not S140), which provides both BLE and ANT+ support. Key points:
- Application flash starts at
0x31000(S340 memory map) - Communicates with Raspberry Pi over USB serial (not BLE)
- Broadcasts speed, cadence and grade over BLE to Garmin Fenix 8 (the Pi
pushes cadence/grade via
BLE_CAD/BLE_INCLIN; the firmware keeps onboard fallbacks) - LSM303DLHC (accel/mag) + VL53L4CD (ToF) share the main sensor PCB
- Arduino IDE with Adafruit nRF52 BSP (modified for S340)
- J-Link EDU Mini used for initial SoftDevice flashing and recovery
- UF2 uploads work normally after initial setup
The S340 is a closed-source SoftDevice distributed by Garmin Canada. To flash it, follow the instructions in this merged PR on the Adafruit nRF52 Bootloader repo:
In short:
- Register an ANT+ Adopter account at thisisant.com (access granted within ~1 business day)
- Download the S340 v7.0.1 SoftDevice from the ANT+ resources page
- Follow the PR's
readme.mdto place and rename the files in the bootloader repo - Uncomment the evaluation key in
nrf_sdm.h(line 191) - Flash the bootloader with
SD_NAME=s340using a J-Link
β οΈ The evaluation key is for development only. A commercial license key from Garmin/Dynastream is required for any product release.
- Random GPS route generation for non-interval Strava exports (Quebec + worldwide)
- Web-based dashboard (Python HTTP server + React kiosk UI)
- Real cadence detection from the frame accelerometer (Garmin footpod parity)
- Improved enclosure design with snap-fit
This project is licensed under the GNU General Public License v3.0 β see the LICENSE file for details.
- Adafruit β Feather nRF52840, sensors, and learning resources
- JLCPCB β PCB fabrication and SMT assembly
- bleak β Python BLE library
- Strava API β Activity upload integration
Built with β€οΈ on a treadmill in MontrΓ©al






