https://github.com/korniychuk/treadmill-bluetooth-macos
π macOS BLE daemon for a Yesoul treadmill β live stats, step goals & speed control (Rust)
https://github.com/korniychuk/treadmill-bluetooth-macos
ble bluetooth bluetooth-low-energy corebluetooth fitness ftms rust treadmill yesoul
Last synced: 29 days ago
JSON representation
π macOS BLE daemon for a Yesoul treadmill β live stats, step goals & speed control (Rust)
- Host: GitHub
- URL: https://github.com/korniychuk/treadmill-bluetooth-macos
- Owner: korniychuk
- License: mit
- Created: 2026-07-04T15:47:28.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2026-07-06T12:37:40.000Z (about 1 month ago)
- Last Synced: 2026-07-06T14:12:47.223Z (about 1 month ago)
- Topics: ble, bluetooth, bluetooth-low-energy, corebluetooth, fitness, ftms, rust, treadmill, yesoul
- Language: Rust
- Homepage:
- Size: 1.44 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
Awesome Lists containing this project
README
# π treadmill-bluetooth-macos
[](https://github.com/korniychuk/treadmill-bluetooth-macos/actions/workflows/ci.yml)
[](./LICENSE)
[](#-limitations)
[](https://www.rust-lang.org)
A **macOS** Bluetooth Low Energy connector for a **Yesoul** treadmill, written in **Rust** π¦.
It discovers the treadmill over BLE (CoreBluetooth), connects, and streams live
telemetry β speed, distance, steps β over the standard **Fitness Machine Service**
(FTMS) GATT profile. A background daemon keeps the link alive, detects whether you
are actually walking (presence), splits your day into workouts, tracks step goals,
and can drive the treadmill (start / stop / target speed). ππ¨
> β οΈ **Unofficial.** Not affiliated with Yesoul. The BLE protocol was
> reverse-engineered against a single unit (Yesoul W2 Pro). It speaks generic
> FTMS, so other FTMS treadmills *may* work, but only the W2 Pro is verified.
> See [Limitations](#-limitations).
---
## β¨ Features
- π **Scan & connect** to the first FTMS treadmill nearby.
- π **Live telemetry** β speed, distance, steps β streamed and logged.
- π§ **Presence detection** β belt moving but steps not rising β "away while
running"; workouts are split on real activity, not wall-clock.
- π **Daily stats** β steps / distance / walking-time, restart-safe.
- β€οΈ **Heart rate** β pair a chest-strap BLE sensor (e.g. Polar H10) and get a
compact `β₯ avg/max` summary alongside your steps, plus a live bpm in the
widget and a sensor-battery check (`tm status`, low-battery glyph in the
widget). Optional β everything degrades silently when no sensor is worn.
- π― **Step-goal milestones** β up to 3 daily goals with native macOS toasts.
- ποΈ **Control** β start / stop / set target speed on a live link.
- π₯οΈ **tmux status-bar widget** β see the current workout in your status line
(see [`scripts/tmux/`](./scripts/tmux)).
- π **Self-healing daemon** β auto reconnect, watchdog, pause/resume speed
restore, AC-power awareness (won't drain the battery when idle).
## πΈ Demo
**Live workout in your tmux status bar** (walking = green, paused = yellow):

**Native macOS toasts** for milestones, presence and connection state:
**CLI** β control the belt, then read stats & status:

## π Requirements
- π **macOS** (Apple Silicon or Intel). No Linux / Windows β see [Limitations](#-limitations).
- π‘ Bluetooth, and a treadmill exposing FTMS (`0x1826`).
- π¦ To build from source: **Rust 1.95+** (edition 2024). `rustup` recommended.
- π¨ To regenerate the app icon only: Xcode Command Line Tools (`swift`).
## π Quickstart (from source)
```bash
git clone https://github.com/korniychuk/treadmill-bluetooth-macos.git
cd treadmill-bluetooth-macos
cargo run # scan: list nearby BLE devices (diagnostic)
cargo run -- connect # connect to the first FTMS treadmill and stream data
```
On first run macOS asks for **Bluetooth permission** β grant it, otherwise the
scan returns nothing. (The permission is attributed to a stable app identity via
an embedded `Info.plist`; see [`docs/tasks/002`](./docs/tasks/002-macos-bluetooth-permission.md).)
Verbose logs:
```bash
RUST_LOG=debug cargo run -- connect
```
## ποΈ Install the background daemon
The daemon auto-connects, tracks presence and stats, shows toasts, and owns the
BLE link so CLI control commands can be routed to it.
```bash
scripts/install-daemon.sh # build, code-sign, install LaunchAgent, symlink `tm`
scripts/uninstall-daemon.sh # remove the LaunchAgent (keeps your data)
```
`install-daemon.sh` builds a release binary, code-signs it, registers a
notification identity, writes a **LaunchAgent** (`~/Library/LaunchAgents/β¦`,
auto-starts at login), and symlinks a short `tm` alias into `~/.bin` so you can
run `tm stats` / `tm status` from anywhere. Add `~/.bin` to your `PATH` if it
isn't already.
> π **Re-run `install-daemon.sh` after every rebuild** β it re-signs and reloads
> the LaunchAgent. A bare `cargo build` leaves the daemon pointing at a stale or
> differently-signed binary.
### πΉοΈ Commands
```bash
tm # = scan: list nearby BLE devices
tm connect # connect and stream (foreground)
tm daemon # run the background loop (normally launched by launchd)
tm stats # today's stats; tm stats --all β every day
tm status # daemon / connection / presence snapshot
tm widget # compact TSV of the current workout (for status bars)
tm hr # diagnostic: connect to a heart-rate sensor, print battery + live bpm
tm speed # set target speed on the live link
tm start | tm stop # start / stop the belt
tm recompute-segments # rebuild workout segments from raw samples (no BLE)
tm default-speed # show the computed default start speed (no BLE)
tm --help # full command list
```
## β¬οΈ Install a prebuilt binary (no Rust needed)
Each tagged release ships an **unsigned, ad-hoc** macOS binary (Apple Silicon)
as a `.tar.gz` on the
[Releases](https://github.com/korniychuk/treadmill-bluetooth-macos/releases) page.
One-liner β grab the latest and install it as a daemon:
```bash
curl -fsSL https://github.com/korniychuk/treadmill-bluetooth-macos/releases/latest/download/treadmill-bluetooth-macos-macos-arm64.tar.gz | tar -xz
cd treadmill-bluetooth-macos-macos-arm64
./scripts/install-prebuilt.sh
```
`install-prebuilt.sh` strips the quarantine attribute, ad-hoc-signs the binary,
installs it to a stable location, registers the notification identity, and loads
the LaunchAgent β **no cargo, no toolchain**. To sign with your own certificate
for a rebuild-stable Bluetooth grant, pass `IDENTITY=""`.
> π **Apple Silicon only** for prebuilt binaries (CI builds `arm64`). On Intel,
> build from source (see [Quickstart](#-quickstart-from-source)).
Prefer to just run it by hand instead of installing the daemon:
```bash
xattr -d com.apple.quarantine ./treadmill-bluetooth-macos
./treadmill-bluetooth-macos connect
```
## βοΈ Configuration
Per-user config (TOML) lives **outside this repo**, at:
```
~/.config/treadmill-bluetooth-macos/config.toml
```
Copy [`config/config.example.toml`](./config/config.example.toml) there and edit it
(every key is optional; the example documents each default as a commented line):
```toml
goals = [8000, 10000, 12000]
# workout_gap_minutes = 15
# auto_pause_minutes = 5
```
- π― `goals` β up to 3 thresholds; each is celebrated once per day with a toast.
- β±οΈ `workout_gap_minutes` (optional, default 15) β segments closer than this merge
into one displayed workout. Applied **at read time**, so changing it is
retroactive; no recompute needed.
- βΈοΈ `auto_pause_minutes` (optional, default 5, `0` = off) β how long the belt may
keep running while nobody is walking (you stepped off) before the daemon pauses
it; the machine's own shutoff then powers it down.
Edits are **hot-reloaded** by the daemon within ~5s while it is connected to the
treadmill (see [`docs/tasks/017`](./docs/tasks/017-hot-reload-goals-config.md)).
A missing file is fine (built-in defaults `[8000, 10000, 12000]`); a malformed
file logs a WARN and falls back to defaults. Override the path with the
`TREADMILL_CONFIG` env var. `tm status` shows the config the daemon currently
has loaded and when it last read it.
## π₯οΈ tmux status-bar widget
`tm widget` prints a compact, tab-separated line for a status bar (empty output
when the treadmill is off, so the segment hides). A reference renderer for
**Dracula** (and a plain-tmux variant) lives in
[`scripts/tmux/`](./scripts/tmux) β see its README for the install recipe and
the exact output contract.

## β οΈ Limitations
- π **macOS only.** The permission flow, notifications, code-signing, LaunchAgent
and IOKit/CoreFoundation glue are all macOS-specific. No Linux / Windows.
- π§ͺ **Verified on one device** β Yesoul W2 Pro (`FW SDC_W2_BT_V3.03-50-54`). Written
as generic FTMS, so other FTMS treadmills may work, but are untested.
- β°οΈ **No incline.** The W2 Pro does not expose inclination over FTMS
(`SetTargetInclination` β *Operation Failed*, no `0x2AD5`). Incline is remote-only.
- ποΈ **Partial control.** Start / stop and target-speed are implemented and
hardware-verified. Incline is not. LED backlight control is
[backlog](./docs/backlog/004-led-control-via-hci-capture.md), not started.
- π **Not in the macOS Bluetooth menu β by design.** The treadmill is app-managed
BLE, without OS-level pairing/bonding (avoids a race for the single BLE central
with the phone app). See [ADR 0001](./docs/adr/0001-no-macos-bluetooth-device-list.md).
Status is via `tm status` / the widget, not System Settings.
- π **Code signing.** Without your own signing identity, macOS re-prompts for
Bluetooth on every rebuild (ad-hoc signatures change the cdhash). Release
binaries are **not notarized** β Gatekeeper will warn unless you strip
quarantine or build locally.
## π§βπ» Development
```bash
cargo test # unit tests
cargo clippy # lints
cargo fmt # format
```
CI (GitHub Actions, `macos-latest`) runs fmt / clippy / build / test on every
push and PR. See [`CONTRIBUTING.md`](./CONTRIBUTING.md).
Architecture and protocol notes live in [`CLAUDE.md`](./CLAUDE.md); research,
decisions (ADRs) and the task journal live in [`docs/`](./docs).
## π License
MIT β see [`LICENSE`](./LICENSE).